TypeScript学习文档

TypeScript

常用类型

类型注解

TS是JS的超级,所有JS代码都是合法的TS代码

通过类型注解可以为变量设置类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let a: string = "hello";
let b: number = 123;
let c: boolean = false;

function greet(name: string, times: number): string {
const str = `你好${name}`;

for (let i = 0; i < times; i++) {
console.log(str);
}

return str;
}

greet("孙悟空", 5);

类型推断

当我们没有明确指定一个变量的类型时,TS会自动对变量的类型进行推断,所以,通常情况下我们不需要为变量设置类型注解,都交给TS自动推断

1
2
3
4
let a: string = "hello"; // string 写的几率很小
let b; // any 这种情况下,无法推断出正确的类型,必须手动注解
b = 123; // any
b = "hello"; // any

原始值

  1. number
  2. string
  3. boolean
  4. bigint 大整数
  5. symbol 符号
  6. undefined
  7. null
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let a: number = 10;
a = 0x123;
a = 0b1111;
a = 0o1234;
a = NaN;
a = Infinity;

let b: string = "hello";
b = `hello`

let c: boolean = true;
c = false;

let d: bigint = 100n;

let e: symbol = Symbol();

数组

  1. Array

  2. T[] (推荐使用)

1
2
3
4
5
let a: Array<number>;
a = [1, 2, 3, 4, 5];

let b: string[];
b = ["a", "b", "c"];

any

  • 表示任意值
  • 当我们为一个变量设置any后,它可以接受任何值
  • 同时它也可以赋值给任意类型的变量
  • 当我们为一个变量设置any类型后,那么它将跳过所有的TS的检查,可以对它调用任意属性或方法,这将导致出现运行时异常的概率大大增加
  • 基于以上特点,在开发时尽量不要使用它
1
2
3
4
5
6
7
8
let a: any;
a = 123;
a = "hello";
a = [1, 2, 3, 4];

let b: boolean = a;

let c; // 声明变量,没有注解也没有赋值,类型会被推断为any

unknow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 
unknown
- 未知类型
- 它的作用和any很像,可以为一个unknown类型的变量赋任意类型的值
但是却不能任意的使用unknown类型
- 要使用unknown类型的对象,需要结合类型断言或类型守卫
- unknown 是 TypeScript 中的一个特殊类型,代表“任何类型”。但与 any 不同的是,unknown 类型更加安全。你不能对一个 unknown 类型的变量直接进行任何操作(比如访问属性、调用方法等),除非你先明确了它的具体类型。
*/

let a: unknown;
a = 123;
a = "hello";
a = [1, 2, 3];
console.log((<number[]>a).length); // 断言
console.log((a as number[]).length); // 断言

a = "hello";
if (typeof a === "string") {
a.toUpperCase();
}

let b: unknown = a;
let c: any = a;

对象类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/* 
object 表示一个对象(除了原始值以外的任何值)
- object所表示的对象的范围太过宽泛
- 所以调用object的属性或方法时,必须要进行类型检查
- 开发时不推荐使用object

{} 使用对象字面量可以直接声明一个类型(不便于重复使用)
语法:
{
属性名:类型;
...
可选属性?:类型;
}

class 直接使用class来声明对象的类型
- 使用class进行类型注解,类本身也会被编译到js文件中

interface TS中独有的,JS并不支持
- 它的定义方式和类相似
- 注意:interface只用来做类型注解,无法用来创建对象,同时不会被编译到JS文件中
*/

let obj: object = { name: "孙悟空" }

if ("name" in obj) {
console.log(obj.name);
}

let obj2: { name: string; age?: number } = { name: "孙悟空", age: 18 }
console.log(obj2.name, obj2.age);

class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}

let p1: Person = { name: "孙悟空", age: 18 }
let p2: Person = { name: "猪八戒", age: 28 }

interface Dog {
name: string;
age: number;
}

let d1: Dog = { name: "旺财", age: 5 }

联合类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* 
联合类型
- 联合类型允许某个值可以是多种类型的一种
- 使用联合类型时只能访问联合类型中共有的属性和方法
- 如果想访问某个类型独有的属性或方法,需要使用类型守卫或类型的断言
*/

let a: string | number | boolean;
a = "hello";
a = 123;
a = true;

interface Person {
name: string;
age: number;
}

interface Dog {
age: number;
}

let someOne: Person | Dog = { name: "孙悟空", age: 18 }
someOne = { age: 22 }

function fn(personOrDog: Person | Dog) {
personOrDog.age;
(personOrDog as Person).name;
if ("name" in personOrDog) {
personOrDog.name;
}
}

交叉类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 
交叉类型:
- 交叉类型使用&连接,要求值必须满足多种类型的要求
*/

interface Named {
name: string;
}

interface Aged {
age: number;
}

let a: Named & Aged = { name: "孙悟空", age: 18 }

类型别名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 
类型别名
- 就是为类型起的别名,使用type关键字来创建
- 注意:类型别名不能重名
*/

type str = string;
type bool = boolean;
type strOrNumOrBool = string | number | boolean;

let a: strOrNumOrBool;

interface Named {
name: string;
}

interface Aged {
age: number;
}

type Person = Named & Aged;

接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/* 
接口(interface)
- 接口和类的作用很像,接口是用来定义对象的结构的,而类是用来创建对象
- 定义接口后,在编译后的js文件中是看不到接口的
- 定义接口时,我们无需关心属性和方法的具体实现
- 可以在接口中定义,一个对象中含有哪些方法和属性
属性修饰符:
? 可选属性
属性名?:类型
readonly 属性名:类型

方法:
方法名():返回值

- implements,接口除了可以限制对象外,也可以用来限制类的定义
- 通过让一个类来实现某个接口,来对该类进行限制,类去实现接口时,也必须包含接口中所有的属性

- extends,接口自身也可以继承接口
*/
interface Person {
readonly name: string;
age?: number;
sayHello?(): void;
}

const p: Person = { name: "孙悟空", age: 18, sayHello: () => { } }

interface Animal {
name: string;
age: number;
}

interface Snake extends Animal {

}

// 创建一个类来实现Animal接口
class Dog implements Animal {
name: string;
age: number;

constructor(name: string, age: number) {
this.name = name;
this.age = age;
}

bark() {
console.log("汪汪汪");
}
}

class Cat implements Animal {
name: string;
age: number;

constructor(name: string, age: number) {
this.name = name;
this.age = age;
}

miao() {
console.log("喵喵喵");
}
}

function fn(animal: Animal) {
if (animal instanceof Dog) {
animal.bark();
} else if (animal instanceof Cat) {
animal.miao();
}
}

fn(new Dog("旺财", 5));
fn(new Cat("喵喵", 5));

类型断言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 
类型断言
- 通过断言可以手动指定类型,从而让变量的类型更加具体
- 断言的语法:
(<类型>变量).xxxx
- 不支持TSX
(变量 as 类型).xxxx

- 断言只在编译时有效,使用断言时一定要确保类型正确
*/

let str: unknown = "Hello World";

(<string>str).length;
(str as string).length;

字面量类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* 
字面量类型
- 就是将一个值指定为类型
- TS中常用的字面量类型string、number
- 通常来讲,字面量都会结合联合类型使用,用来限制某个值的范围

类型推断
- 当我们为一个常量直接赋值一个字面量时,它的类型会自动被推断为当前的字面量
- 推断对象类型时,let和const没有区别

as const(常量断言)
- 将一个值或对象断言为常量
*/

type Direction = "left" | "right" | "up" | "down";
type DiceValue = 1 | 2 | 3 | 4 | 5 | 6;

let dir: Direction = "right";
let dice: DiceValue = 1 // ctrl + i 智能提示

let a = "hello"; // string
const b = "hello"; // "hello"

let c = { x: 1, y: 2 } // 推断对象类型时,let和const没有区别
const d = { x: 1, y: 2 }

let e = "haha" as const; // 等价于 let e: "haha" = "haha"
let f = { x: 1, y: 2 } as const // 对对象进行常量断言时,对象会变成只读对象

空类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 
null
undefined
- null和undefined在TS中默认是所有类型的子类型,默认情况下null和undefined可以赋值给任意类型
- 但是这样一来会带来很多的隐患,建议在开发时将 strictNullChecks 开启,开启后null和undefined只能赋值给对应的类型
void
- void表示空类型
- 默认情况下void类型的变量可以接收undefined和null,如果开启了strictNullChecks,则只能接收undefined
- void用来指定没有返回的值的函数的返回值类型
*/

function fn(): void {
return undefined;
}

never

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* 
never
- 用来表示永远不可能出现的类型
- 任何值都不能赋值给never
- 使用场景
1.用来设置不可能执行完毕的函数的返回值类型
2.可以用来对类型进行安全检查
*/

function fn(): never {
throw new Error("出错了");
}

// function fn2():never{
// while(true){}
// }

type Direction = "left" | "right";

function fn3(dir: Direction) {
switch (dir) {
case "left":
console.log("向左");
break;
case "right":
console.log("向右");
break;
default:
const unreachabe: never = dir;
break;
}
}

非空断言

1
2
3
4
5
6
7
8
9
10
11
function fn(str: string | null) {
if (typeof str === "string") {
str.length;
}

(str as string).length;

let num = str!.length; // 非空断言,str不为空

let num2 = str?.length; // 可选链
}

控制流分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 
类型收窄
- 将一种宽泛的类型限制为更具体的类型

控制流分析
- 通过控制流分析,我们能够在不同的代码路径中获得变量更精确的类型
*/

function doSomething(value: string | number) {
if (typeof value === "string") {
value.toUpperCase();
} else {
value += 10;
}
}

typeof

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 
typeof
- 在JS中可以通过typeof来检查值的类型,会返回一个字符串作为结果
string、boolean、number、undefined、object、function、bigint、symbol...
- 在TS中如果将typeof用在流程控制语句中,TS会根据typeof的结果自动对类型进行收窄
*/

function doSomething(value: string | number) {
if (typeof value === "string") {
value.toUpperCase();
} else {
value += 10;
}
}

相等性收窄

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/* 
相等性收窄
- 利用相等运算符来对类型进行收窄
- 由于相等和不等在JS中会导致自动的类型转换,所以建议使用全等和不全等进行收窄
*/

function fn(str: string | undefined) {
if (str !== undefined) {
console.log(str.length);
}
}

// 定义一个表示圆形的接口
interface Circle {
kind: "circle";
radius: number;
}

// 定义一个表示正方形的接口
interface Square {
kind: "square";
sideLength: number;
}

type Shape = Circle | Square;

function getArea(shape: Shape): number {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
}

return shape.sideLength ** 2;
}

in运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 
in收窄
- 通过检查类型中是否含有某个属性来收窄类型
*/

// 定义一个表示圆形的接口
interface Circle {
radius: number;
}

// 定义一个表示正方形的接口
interface Square {
sideLength: number;
}

type Shape = Circle | Square;

function getArea(shape: Shape): number {
if ("radius" in shape) {
return Math.PI * shape.radius ** 2;
}

return shape.sideLength ** 2;
}

instanceof

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 
instanceof
- 用来检查某个类的原型是否出现了某对象的原型链
- instanceof只适用检查类和实例的关系
*/

function fn(value: string[] | Date) {
if (value instanceof Date) {
value
} else {
value
}
}

赋值收窄

1
2
3
4
5
6
7
8
9
10
11
12
/* 
赋值收窄
- 当我们为一个联合类型的变量赋一个准确的值时,TS会自动根据所赋的值对变量的类型收窄
*/

let x: string | number | boolean;

x = 10;

x = "hello";

x = true;

类型谓词

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/* 
在对值的类型进行判断时,如果有一些类型需要经常判断,或有些类型的判断比较复杂,
此时我们就习惯性希望可以将这些验证封装到一个函数中,然后通过函数的返回值来判断类型

但是如果我们直接创建一个返回布尔值的函数,对于TS来说这就是一个普通的函数,无法用来进行类型收窄

类型谓词
- 类型谓词是一个特殊返回值的类型,设置后TS会根据这个函数的返回值来对值的类型进行收窄
- p is T
*/

// 定义一个表示圆形的接口
interface Circle {
kind: "circle";
radius: number;
}

// 定义一个表示正方形的接口
interface Square {
kind: "square";
sideLength: number;
}

type Shape = Circle | Square;

// 创建一个函数,用来验证一个Shape是否为圆形
function isCircle(shape: Shape): shape is Circle {
return shape.kind === "circle";
}

function getArea(shape: Shape): number {
if (isCircle(shape)) {
return Math.PI * shape.radius ** 2;
}

return shape.sideLength ** 2;
}

断言函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/* 
断言
- 断定一个事真伪,并在判断错误时抛出异常
断言函数
- 创建一个起到断言作用的函数
- asserts 条件
- 条件就是一个类型谓词
- 断言函数用来为一个类型进行断言,断言后变量的类型就会被TS收窄
如果断言正确,则什么也不需要做,如果断言错误,通常会在断言函数中抛出异常终止程序

- 这个断言函数有什么用?它的使用场景时什么?
*/

interface Circle {
kind: "circle";
radius: number;
}

function isCircle(value: any): asserts value is Circle {
// 断言函数通常不需要返回值,通常会在断言失败时报错
if (value.kind !== "circle") {
throw new Error("value的类型不是Circle");
}
}

let a: unknown = {}

isCircle(a); // 调用了断言函数,那么在函数后边所有的a的类型都会被认为Circle


/*
断言函数实际上是TS中一种运行时的类型检查方式

代码即使写的再好,依然不能完全避免运行时的错误!

比如:当我们去调用后台接口去加载数据时,后台所返回的数据需要经过前端的渲染然后显示
但是我们作为前端是无法保证后台的数据结构的准确的,这时就会引起运行时的异常
*/

// 创建一个数据interface
interface Data {
msg: string;
}

function isData(data: any): asserts data is Data {
if (data.msg === undefined) throw new Error("Data的结构有误!");
}

// 创建一个函数,模拟从后台加载数据的情况
function getData(): Promise<Data> {
return new Promise((resolve, reject) => {
const data = { msg: "这是合法数据!" } // 从服务器加载过来的数据
// 对data做一系列的检查,如果正确则调用resolve()返回数据,错误则调用reject()来报错

// 也可以使用断言函数来断言data的类型
isData(data);

resolve(data);
})
}

getData()
.then(data => console.log(data))
.catch(err => console.log("出错了"));

函数

函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/* 
函数的类型注解
1.使用Function
- 开发时几乎不用,因为它的限定范围过于宽泛,使用意义不大

2.函数类型表达式
- 语法:(参数列表) => 返回值

3.使用调用签名
- 适用于更加复杂的函数
- 调用签名需要在对象或接口中使用,使用后即表示该类型是一个可调用的类型(函数)
*/

let fn: Function;

fn = function () { }
fn = (a: string) => 123;

let fn2: (str: string) => string;
fn2 = (str: string) => "hello";

type MyFunction = (str: string) => string;

// 可调用的对象(函数)
type MyFun2 = {
(a: number, b: number): number;
}

interface MyFun3 {
(a: string, b: string): string
}

let sum: MyFun2 = function (a: number, b: number) {
return a + b;
}

let getStr: MyFun3 = (a: string, b: string) => a + b;

函数的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/* 
函数类型注解
参数:
1.参数名
- 类型的参数名和值的参数名不需要一致
- 最终的函数的签名以类型为准,所以尽量将类型中的参数名写得有意义一些
2.参数的数量
- 值中的参数数量不能多余类型定义的参数数量
- 为了兼容旧版本js函数,所以值中的参数个数可以少于类型中定义的参数个数
3.参数的类型
- strictFunctionTypes 开启时
- 值中的参数类型不能比类型中的参数类型还具体
值的参数类型 > 类型的参数类型
- strictFunctionTypes 关闭时 (双向协变)
- 此时只要值中的参数类型被类型中参数类型所包含即可

返回值:
void
- void表示没有返回值或返回值没用
*/

type MyFunction = (str: string) => string;

let fn: MyFunction = (str: string) => "hello";

type MyFunction2 = (str: string) => string;

let fn2: MyFunction2 = (str: string | number) => "hello";

interface MyObj {
test(str: string | number): void;
}

let obj: MyObj = {
test(a: string) { } // strictFunctionTypes 对于方法无效,所以这里可能会出现运行时异常
}

type MyFunction3 = () => void;

构造签名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 
构造签名的主要作用,是用来描述类的
*/

interface MyFn {
new(str: string): any;
}

let Fn: MyFn = class {
a: string;
constructor(a: string) {
this.a = a;
}
}

new Fn("hello");

使用的场景

1
2
3
4
5
6
7
8
9
type MyFunction = (str: string) => void;

// 直接使用函数类型来限制变量
let fn: MyFunction = (a) => "hello";

// 用来限制回调函数的结构
function eventHandler(event: string, callback: MyFunction) { }

eventHandler("hello", (a: string) => { });

可选参数

1
2
3
4
5
6
7
8
9
10
11
12
/* 
在定义函数类型时,可以将参数设置为可选参数
- 只需要在参数后添加一个?则表示当前的参数是一个可选的参数
- 可选参数可以设置多个,它们必须位于参数列表的最后!
可选参数后边只能是可选参数
*/

type MyFn = (num: number, str?: string) => void

let fn: MyFn = (a, b) => {
if (typeof b === "string") console.log(b.length);
}

剩余参数

1
2
3
4
5
6
/* 
剩余参数 rest
- 剩余参数只能有一个,以...开头,需要添加数组类型的注解
*/

type MyFn = (a: string, b?: number, ...args: any[]) => void;

泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/* 
泛型
*/

// 定义一个函数,用来获取数组的第一个元素
// 使用any类型确实可以满足要求,但与此同时也带来一些隐患(非必要 不any)
/*
问题:
数组的类型只有在调用时才能知道,在定义函数时无法确定数组类型
能不能在调用时再指定类型呢?
*/
// function firstElement(arr: any[]) {
// return arr[0];
// }

// let result = firstElement([1, 2, 3, 4]);

function firstElement<T>(arr: T[]): T {
return arr[0];
}

let r1 = firstElement<string>(["hello", "haha", "xixi"]);
let r2 = firstElement<number>([1, 2, 3, 4]);


/*
泛型
- 泛型是一种在定义函数、类、接口、类型别名时不指定具体类型,而在使用时再指定类型的编程方法
- 泛型也可以被推断,如果使用时不指定泛型的类型,TS会自动推断其类型
*/

function fn<T>(arg: T): T {
return arg;
}

interface MyInterface<T> {
get(): T;
}

let inter: MyInterface<string> = {
get() {
return "hello";
}
}

class MyClass<T>{
property: T;

constructor(property: T) {
this.property = property;
}
}

type MyType<T> = T;

泛型的约束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/* 
泛型的约束
- 在定义泛型时,可以通过extends关键字来对泛型进行约束
- 语法:
泛型 extends 类型
T extends { length: number }
K extends keyof T
*/

function getLength<T extends { length: number }>(a: T): number {
return a.length;
}

getLength("hello");
getLength([1, 2, 3, 4]);

function getProperty<T, K extends keyof T>(obj: T, propName: K) {
return obj[propName];
}

getProperty({ name: "孙悟空", age: 18 }, "age");
getProperty({ name: "孙悟空", age: 18 }, "name");

/*
keyof运算符,可以获取一个类型(接口、类、类型别名)中的所有属性名
*/
interface Person {
name: string;
age: number;
gender: string;
}

type objKeys = keyof Person; // "name" | "age" | "gender"

重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/* 
重载
- 重载指为一个函数提供多个函数签名,根据传递参数的不同调用不同函数起那么
- 一个函数对应多个函数签名
- 语法:
函数签名1
函数签名2
...
函数实现(){}
- 当我们调用重载函数时,TS会自动更具参数的个数类类型自上向下匹配签名
*/

/*
function sum(a: number | string, b: number | string): number | string {
if (typeof a === "number" && typeof b === "number") {
return a + b;
} else if (typeof a === "string" && typeof b === "string") {
return a + b;
}

throw new Error("Type Error");
}

let result = sum(123, 123);
*/

function sum(a: string, b: string): string;
function sum(a: number, b: number): number;
function sum(a: string | number, b: string | number): string | number {
if (typeof a === "string" && typeof b === "string") {
return a + b;
} else if (typeof a === "number" && typeof b === "number") {
return a + b;
}

throw new Error("Type Error");
}

let result = sum(123, 123);


/*
重载签名
实现签名
- 实现签名不会在调用时显示
- 实现签名应该覆盖重载签名的所有参数和返回值

- 调用函数时,实参必须能和某个重载签名完全匹配,否则会报错
- 如果能够使用联合类型来实现,就尽量不用重载
*/

function fn(a: string): void;
function fn(a: boolean, b: number): void;
function fn() { }

function fn2(a: string): void;
function fn2(a: boolean, b: number): void;
function fn2(a: string | boolean, b?: number) { }

function len(str: string): number;
function len(arr: any[]): number;
function len(value: string | any[]): number {
return value.length;
}

len("hello");
len([1, 2, 3, 4]);

// 错误的
// len(Math.random()>0.5 ? "hello" : [1,2,3,4]); // 这个三元表达式的类型是 string | number[]

function len2(val: string | any[]): number {
return val.length;
}

len2("hello");
len2([1, 2, 3, 4]);
len2(Math.random() > 0.5 ? "hello" : [1, 2, 3, 4]);

this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/* 
this
- 由于我们开启了严格模式,直接访问this会出现问题
- noImplicitThis 开启后会禁止有隐式的any的类型
- 在函数中可以通过第一个参数来为this做类型注解
- 在箭头函数中不能指定this
*/

function fn(this: any) {
console.log(this);
}

class Person {
name: string;
age: number;

constructor(name: string, age: number) {
this.name = name;
this.age = age;
}

sayHello(this: Person) {
console.log(this.name);
}

test = () => {
console.log(this.name);
}
}

function filterPerson(filter: (this: Person) => boolean) { }

filterPerson(function () {
this.name;
return true;
});

// 错误代码
/* filterPerson(()=>{
this.name;
return true;
}); */

对象

可选和只读

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Person {
readonly name: string; // readonly 表示只读属性
age?: number; // ?表示可选属性 类型变为number | undefined
}

// 当我们为一个接口类型直接赋值一个字面量时,会进行额外属性检查
const p: Person = { name: "孙悟空", age: 18 }

// 属性的只读只是在TS编译时做的限制,并没有在JS层面属性产生实质影响
const obj: { name: string; age?: number } = p;

obj.name = "猪八戒";
console.log(p.name); // 猪八戒

索引签名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/* 
索引签名
- 用来设置类型中索引(属性、键)和值的类型
- 语法:[名字:类型]:类型
- TS中的属性名的类型:
string
number
symbol
*/

interface Inter {
[propName: string]: string | number;
}

interface Inter2 {
[index: number]: number;
}

// 在对象中,所有的数字为属性的名的属性,最终属性名都会转换为字符串
const obj: Inter = { name: "孙悟空", gender: "男", age: 18, 0: 123 }
const obj2: Inter2 = [123, 456];


interface Inter {
[key: symbol]: string;
}

// 创建一个符号
let s = Symbol();

let obj: Inter = { [s]: "hello" }

console.log(obj[s]);

// 索引签名可以混合使用
// 下一个例子,就避免额外属性检查
interface Inter2 {
[key: string]: any;
name: string;
gender: string;
age: number;
}

interface Inter3 {
[propsName: string]: string | number;
[index: number]: number
}

let obj2: Inter2 = { name: "孙悟空", gender: "男", age: 18 }

let obj3: Inter3 = { name: "孙悟空", 0: 123, 1: 456 }

接口继承和交叉类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/* 
接口继承:
- 继承后子接口将拥有父接口中全部属性定义
- 继承可以多重继承
- 接口之间是继承,类和接口之间是实现,类和类之间是继承

交叉类型:
- 交叉类型混合两种类型

区别:
- 继承发生在接口(或类)之间,交叉类型只要是类型就可以进行
- 发生冲突属性时,接口会报错!交叉类型会合并出never
*/

interface A {
name: string;
}

interface B {
age: number;
}

interface C extends A, B {
address: string;
gender: string;
}

class D implements A, B {
name: string = "孙悟空";
age: number = 18;
}

type E = A & B;
let e: E = { name: "孙悟空", age: 18 }

interface F {
name: string;
gender: string
}

interface G {
age: number;
gender: boolean;
}

// interface H extends F,G{}
type H = F & G;

只读数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 
只读数组
- 数组中的元素只能读不能修改
- ReadonlyArray<元素类型>

只读集合
- Map
- ReadonlyMap<键的类型, 值的类型>
- Set
- ReadonlySet<值的类型>
*/

const arr: ReadonlyArray<number> = [1, 2, 3, 4, 5];
const arr2: readonly number[] = [1, 2, 3, 4, 5];

const map: ReadonlyMap<string, number> = new Map([["one", 1], ["two", 2]]);

const set: ReadonlySet<string> = new Set(["孙悟空", "猪八戒", "沙和尚"]);

元组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* 
元组(tuple)
- 元组是一个特殊的数组
- 使用时可以为每个元素分别指定类型,同时可以限制元素的数量
- 除了数量固定,类型分别指定外,元组和数组使用的方式是一样
- 元组适用于强约定的API
*/

let tuple: [string, number] = ["hello", 123];

function fn(person: [string, number, string]) { }

function fn2() {
return [() => { }, true, false, null];
}

function fn3(a: string, b: number, ...args: [string, number, boolean]) { };

fn3("hello", 123, "aaa", 456, true);

let tuple2: readonly [string, number] = ["hello", 123];

/*
元组也支持可选元素和可变类型
*/
let tuple3: [string, number?] = ["hello", 123];

let tuple4: [string, number, ...boolean[]] = ["aaa", 123, true, false];
let tuple5: [string, ...boolean[], number] = ["aaa", true, false, 123];
let tuple6: [...boolean[], string, number] = [true, false, "aaa", 123];

获取类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/* 
keyof
- 用来获取类型的所有属性
typeof
- 在ts中可以通过typeof来获取一个值的类型
- 对于原始值就是返回对应的类型,
对于常量(或as const)返回的是字面量类型
- 对于对象类型,返回对象的类型签名
如果使用常量断言,则获取到的对象的属性都是只读的!
属性类型
- 通过 类型["属性名"] 来获取属性的类型
*/

interface Person {
name: string,
age: number,
gender: string
}

type PersonProerties = keyof Person; // "name" | "age" | "gender"

let a = "hello";
// const a = "hello";
// let a = "hello" as const;
let b: typeof a = "哈哈哈";

let obj = {
name: "孙悟空",
age: 18
}

type GetType = typeof obj;

type PropertyType = Person["name"]; // 获取Person的name属性的类型
type PropertyType2 = typeof obj.name;

条件类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 
条件类型
- 根据不同的条件返回不同的类型
- 语法:
T extends U ? X : Y
- 条件类型在日常常规开发中使用几率并不高
它主要用在开发中编写一些类型工具
*/

type isString<T> = T extends string ? "yes" : "no";

let b: isString<number> = "no";

// 获取数组类型的元素类型
// infer 用来表示TS自动推断
type NumberArr = string[];

type ElementType<T> = T extends Array<infer U> ? U : T;

let c: ElementType<NumberArr>;
let d: ElementType<boolean>;

映射类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/* 
映射类型
- 根据已有的类型创建一个新的类型
- 映射类型只支持对象字面量的语法
- 创建映射类型时,可以根据需要对原类型中的属性进行修改
- 内置工具类:
Readonly<T> 创建所有属性都是只读的T类型
Partial<T> 创建所有属性都是可选的T类型
- 映射修改器:
+readonly
-readonly
+?
-?
*/

interface Person {
name: string;
age: number;
address: string;
}

// MyPerson是对Person结构的完全复制
type MyPerson = {
[P in keyof Person]: Person[P];
}

// 将所有属性设置为只读属性
type ReadonlyPerson<T> = {
readonly [P in keyof T]: T[P];
}

let p: Readonly<Person> = { name: "孙悟空", age: 18, address: "花果山" }

// 将所有属性设置为可选属性
type Partial<T> = {
[P in keyof T]?: T[P];
}


interface Animal {
name?: string;
age: number;
}

type MyAnimal = {
[P in keyof Animal]-?: Animal[P]; // 去除属性的可选性
}

工具类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/* 
工具类型
- TS中为我们提供的一组用于操作类型的方法
- 泛型中的字母:
T Type 类型
U 第二个类型
K Key 键(属性名)
- 例子:
Partial<T> 将属性设置为可选属性
Readonly<T> 将属性设置只读
Required<T> 将属性变为必须的属性
Pick<T, K> 从T类型中挑选出指定属性K
Omit<T, K> 从T类型中移除指定属性K
Record<K, T> 创建一个新的类型
Exclude<T, U> 将U中的类型从T中排除
Extract<T, U> 将T中符合U的类型取出
NonNullable<T> 将类型中空值和undefined去除
*/

interface Todo {
title?: string;
description?: string;
}

let todo: Required<Todo> = { title: "hello", description: "哈哈哈哈" }

function printObj<T>(obj: Required<T>) {
console.log(obj);
}

printObj<Todo>({ title: "hello", description: "哈哈哈哈" });

/*
Pick和Omit
*/
interface Person {
name: string;
age: number;
gender: string;
address: string;
}

let p1: Pick<Person, "name" | "age"> = { name: "孙悟空", age: 18 }

let p2: Omit<Person, "name" | "age"> = { gender: "男", address: "花果山" }


/* Record */
type MyType = Record<"name" | "gender" | "address", string>;

let personRecord: Record<string, Person> = {
tom: { name: "tom", age: 18, gender: "男", address: "花果山" },
tom2: { name: "tom2", age: 18, gender: "男", address: "花果山" }
}


/*
Exclude
Extract
*/
// Exclude
type M1 = Exclude<"a" | "b" | "c", "b">; // "a" | "c"
type M2 = Exclude<"a" | "b" | "c", "a" | "b">; // "c"

// Extract
type M3 = Extract<"a" | "b" | "c", "a">; // "a"
type M4 = Extract<"a" | "b" | "c", "a" | "c">; // "a" | "c"


/* NonNullable */
type T1 = NonNullable<string | null | undefined | number>;

const removeNullFromArray1 = <T>(array: T[]): T[] => {
return array.filter(item => item !== null && item !== undefined);
}

let result1 = removeNullFromArray1([1, 2, 3, null, undefined, 123]); // (number | null | undefined)[]

function removeNullFromArray2<T>(array: T[]): NonNullable<T>[] {
return array.filter(item => item !== null && item !== undefined) as NonNullable<T>[];
}

let result2 = removeNullFromArray2([1, 2, 3, null, undefined, 123]); // number[]
1
2
3
4
5
6
7
8
9
10
11
interface Person {
name: string;
age: number;
gender: string;
address: string;
}

let personRecord: Record<string, Person> = {
tom: { name: "tom", age: 18, gender: "男", address: "花果山" }, // 这是一个 string 类型的键
[s]: { name: "tom2", age: 18, gender: "男", address: "花果山" } // 这是一个 symbol 类型的键
};

TypeScript 对这个赋值操作的理解是:

  • tom 属性'tom' 是一个字符串,符合 [key: string] 的索引签名规则。所以 tom 属性的检查通过了。
  • [s] 属性s 是一个 Symbol。虽然 Record<string, Person> 的索引签名只定义了 string 类型的键,但 TypeScript 认为,在对象创建时就明确写出的属性,是一种“显式声明”,而不是通过动态字符串访问。它允许这种显式声明的 Symbol 键与 string 索引签名并存。

可以理解为,索引签名主要用于控制后续的、动态的属性访问,比如:

1
2
3
4
personRecord["some_other_key"] = { ... }; // OK,因为 "some_other_key" 是字符串
personRecord[Symbol("another_id")] = { ... }; // 这里会报错!
// Error: Element implicitly has an 'any' type because expression of type 'symbol' can't be used to index type 'Record<string, Person>'.
// No index signature with a parameter of type 'symbol' was found on type 'Record<string, Person>'.

当试图在对象创建之后通过一个 Symbol 去给 personRecord 赋值时,TypeScript 就会严格按照 Record<string, Person>string 索引签名进行检查,并告诉你没有为 symbol 类型定义索引,从而报错

  • 编译时不报错:因为在对象字面量初始化时,TypeScript 允许你定义 JavaScript 中所有合法的键类型(string, number, symbol),即使类型定义中的索引签名只指定了 string。它将 [s] 视为一个“已知”的、显式定义的属性,而不是一个需要匹配 [key: string] 模式的动态属性。
  • 使用时会报错:当你试图在对象创建后,通过一个 symbol 变量去动态访问或赋值时,TypeScript 就会严格执行索引签名的规则,此时就会报错。

字符串工具类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 
Uppercase<StringType>
- 将字符串字面量类型转换为大写
Lowercase<StringType>
- 将字符串字面量类型转换为小写
Capitalize<StringType>
- 将字符串字面量类型转换为首字母大写
Uncapitalize<StringType>
- 将字符串字面量类型转换为首字母小写
*/

type MyType = "hello";

let a: Uppercase<MyType> = "HELLO";

let b: Lowercase<"HELLO"> = "hello";

let c: Capitalize<MyType> = "Hello";

let d: Uncapitalize<"HELLO"> = "hELLO";

映射类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/* 
通过映射类型来修改属性名
- 可以通过as来修改映射类型中属性名
- 例子:
[P in keyof Person as "hello"]
"name" -> "hello"
[P in keyof Person as `my${Capitalize<P>}`]
"name" | "age" -> "myName" | "myAge"

*/

interface Person {
name: string;
age: number;
}

type NewPerson = {
[P in keyof Person as `my${Capitalize<P>}`]: Person[P];
}

interface Person2 {
name: string;
age: number;
1: number;
}

type NewPerson2 = {
[P in keyof Person2 as `${string & P}`]: Person2[P];
}
/*
[P in keyof Person2 as `${string&P}`]
"name" | "age" | 1 --> "name" | "age" | never(不存在,移除)
*/

type NewPerson3 = {
[P in keyof Person2 as `${Exclude<P, 1>}`]: Person2[P];
}

type NewPerson4 = {
[P in keyof Person2 as `${Extract<P, "name" | "age">}`]: Person2[P];
}

type NewPerson5 = {
[P in keyof Person2 as `get${Capitalize<string & P>}`]: Person2[P];
}

type NewPerson6 = {
[P in keyof Person2 as P extends string ? `get${Capitalize<P>}` : never]: Person2[P];
}

模板字符串类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type Fruit = "apple" | "orange" | "banana";

/*
模板字符串定义字面量类型,可以和其他类型进行拼接
*/

type Hello = `Hello ${Fruit}`;

// 监视对象的属性变化,获取到新的值
/*
obj : 被监视的对象
eventName : 事件名 - xxxChanged
callback : 回调函数
*/
function eventHandler<T, K extends string & keyof T>(
obj: T,
eventName: `${K}Changed`,
callback: (newValue: T[K]) => void
) { }

eventHandler(
{ name: "孙悟空", age: 18, gender: "男" },
"ageChanged",
(newValue) => { }
)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/* 
面向对象(oop)
- JS是一个面向对象的编程语言
- 面向对象指程序中的所有操作都需要通过对象来完成
对象(object)
- 编程语言是对现实事物的抽象
- 具体 抽象
- 具体的事物抽象到计算机后,就变成了一个对象
- 例子:
浏览器窗口 --> window对象
网页 --> document对象
控制台 --> console对象
程序是如何对一个具体事物进行抽象的?
- 一个事物再复制无非由两部分组成:
1.数据(属性)
2.行为(方法)

类(class)
- 对象的模板,通过类可以批量创建对象
- 使用class关键字
- 类由两部分构成:
属性(properties)
- 属性、字段、成员变量
- 直接在类中定义
- 如果开启了 strictPropertyInitialization 选项
则要求属性必须进行正确的初始化
- 初始化:
1.可以直接在声明处初始化 name = "孙悟空"
2.可以在构造函数中对属性进行初始化

方法(methods)
-

*/

class Person {
name: string;
age: number;
gender!: string; // 非空断言,不需要在类中对属性进行初始化,但是一定记得要在其他地方初始化

constructor(name: string, age: number) {
this.name = name;
this.age = 10;
}

// 正常的方法,添加到原型上
seyHello() {
console.log(`大家好!${this.name}在这里祝大家快乐`);
}

// 通过属性的方式添加的方法,添加到实例上
// 不推荐使用
test = () => {
console.log("测试方法", this.name);
}
}

const p1 = new Person("孙悟空", 18);

console.log(p1);

构造函数

1
2
3
4
5
6
class MyClass {
// 构造函数的重载
constructor(x: number, y: string);
constructor(x: boolean);
constructor(x: number | boolean, y?: string) { }
}

属性访问器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/* 
属性的访问器(Getter Setter)
- 操作属性的方法
- 通过他们可以避免直接去修改和读取属性
- 使用访问器的优点
1.可以在读取和设置属性时添加逻辑
2.可以方便我们控制属性的访问权限
*/

class Person {
// #标识的属性时私有属性,只能在类内部使用!这是JS的语法
#name: string;
#age: number;

constructor(name: string, age: number) {
this.#name = name;
this.#age = age;
}

// get方法用来返回属性值
get name() {
console.log("get执行了");

return this.#name;
}

set name(name: string) {
console.log("set执行了");

this.#name = name;
}
}

let p = new Person("孙悟空", 18);

p.name;
p.name = "猪八戒";

继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/* 
继承:
1.方便代码复用
2.方便扩展(OCP原则)
3.多态
4.JS的语法

继承时发生了什么事:
1.创建类时JS引擎会为每一个类都定义一个原型对象
2.当一个子类继承父类时,子类的原型会被设置为父类原型的实例
Child[[Prototype]] = 父类原型的实例
3.当我们调用子类的构造函数创建子类实例时,JS引擎会自动调用super()
来使用父类的构造函数先初始化子类
*/

// 父类 超类
class A {
name: string;

constructor() {
console.log("父类A的构造函数执行了!!");
this.name = "孙悟空";
}

method() {
console.log("method");
}
}

// 子类 派生类
class B extends A {
// 继承后,如果需要重写构造函数,在构造函数第一件事就是调用super()
constructor() {
super();
}

newMethod() {
// 调用父类的方法
super.method();
console.log("b添加的方法");
}
}

const b: A = new B();

b.method();

// 多态
function fn1(arg: A) { }

fn1(b);

class Animal {
name = "动物";
}

class Dog extends Animal { }
class Cat extends Animal { }

function fn2(arg: Animal) { }

fn2(new Dog());

成员修饰符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* 
成员修饰符
- 用来修饰类中的属性和方法
public 默认值,公共属性,这个属性可以在任何位置使用
protected 受保护的属性,只能在类内部和子类内部访问
private 私有属性,只能在类内部访问
*/

class Myclass {
public name = "孙悟空";
protected age = 18;
private gender = "男";

protected sayHello() {
this.name = "xxx";
}
}

class A extends Myclass {
test() {
this.age;
}
}

const mc = new Myclass();

静态修饰符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/* 
静态属性(方法)
- 通过类直接访问的属性(方法)
类.属性名
- 注意:
静态属性和静态方法是直接存储到类中的!
所以在静态方法中不能访问实例属性(方法)
在静态方法中this就是当前的类!

静态属性可以通过#设置为私有,设置后只能在内部访问

静态属性不能使用泛型
*/

class Myclass {
age = "123";
static #staticProp = "静态属性";

static staticMethod() {
console.log(this.#staticProp);
}

/* 静态代码块,它里边的代码只会执行一次,并且是在类定义后就执行 */
static {
// 在静态代码块中可以访问类中的静态属性和静态方法
// 通常我们会使用静态代码块对静态属性进行初始化(复制的属性)
this.#staticProp = "我修改一下";
}
}

class Person<T>{
name: T;

// 静态属性不能使用泛型
// static staticProp:T;

constructor(name: T) {
this.name = name;
}
}

实现接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/* 
实现接口
- 使用implements来实现一个接口
- 接口和类不同,接口关心标准
- 多态的体现

实现接口和继承类的区别:
1.接口是TS的语法,而类TS JS都支持
2.定义接口时不关心具体的实现,定义类的时候我们需要考虑细节
3.一个类可以同时实现多个接口,只能继承一个类
4.实现接口和继承可以同时使用
*/

interface Inter {
name: string;
age: number;
gender: string;
}

interface A { }

class B { }

class Person extends B implements Inter, A {
name = "孙悟空";
age = 18;
gender = "男";
address = "花果山";
}

function printInfo(obj: Inter) {
console.log(obj.name, obj.age, obj.gender);
}

printInfo(new Person());

抽象类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* 
抽象类
- 介于接口和类之间
- abstract 关键字来定义一个抽象类
抽象类就是专门用来被其他类继承的
- 特点:
1.抽象类不能创建实例
2.抽象类是专门被继承的类
3.在抽象类中可以添加抽象属性和方法
抽象属性和方法必须在子类中初始化和实现

- 抽象类会被转换为JS代码,但其中加了abstract关键字的属性和方法都会被删除
*/

abstract class MyAbClass {
// constructor(public name:string,public age:number){}

abstract name: string; // 抽象属性
abstract sayHello(): void; // 抽象方法
}

class A extends MyAbClass {
constructor(public name: string) {
super();
}

sayHello(): void {

}
}

其他

命名空间

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 
命名空间(namespace)
- 为了在模块化系统不完善的一个补充
*/

namespace MyModule {
let a = 10;
export let b = "hello";
export function fn() { }
}

MyModule.b;
MyModule.fn;

枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/* 
枚举(Enum)
- 枚举就是常量的集合
- 使用enum关键字来定义一个枚举
默认情况下,成员的值自上而下为0开始的整数
也可以根据需要自行执行成员的值
- 声明枚举时使用const关键字,会使得枚举变为常量枚举
常量枚举使用时会直接转换为字面量不会被编译到js中
所以所有的运行时功能常量枚举都不支持
*/

let status = 1;

enum Status {
Pending,
Approved,
Rejected
}

if (status === Status.Pending) {
console.log("加载中");
} else if (status === Status.Approved) {
console.log("加载完毕。。");
} else if (status === Status.Rejected) {
console.log("加载失败。。");
}

let a = Status.Approved;
let b = Status[2];

console.log(a, b); // 1 Rejected


enum Color {
Red = "red",
Green = "green",
Blue = "blue"
}

function setColor(color: Color) { }

setColor(Color.Red);

/* 常量枚举 */
const enum Num {
One,
Two,
Three
}

声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 
声明(declare)
- 在编译阶段声明一个变量、函数、类型、类或模块等,但是不对其进行实现
- 通过编写声明可以帮助TS来识别JS库的代码
*/

declare let myGlobal: number; // 声明了一个全局变量
declare function printLog(msg: string): void;

declare class JQueryIntance {
text(): string;
}
declare function jQuery(selector: string): JQueryIntance;
declare var $: typeof jQuery;

$("#box1").text();


/*
声明文件(d.ts)
- 声明文件就是专门用来存放声明的文件以 .d.ts 结尾
- 在声明文件中可以省略declare
*/

声明文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import Person, { abc } from "../../types/scope";

export { }

abc.startsWith("a");

const p: Person = {}


/*
声明文件的位置
根目录/tyeps 自己编写的声明文件
模块源码目录 模块作者自己编写的声明文件
node_modules/@types 第三方的声明文件

在TS中使用JS模块时,除了要下载模块本身外还需要下载模块的声明文件,以帮助TS识别类型
npm i @ types/jquery
*/

scope.d.ts

1
2
3
export let abc: string;

export default interface Person { }

装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/* 
在不修改类源代码的情况下,对它进行修改

装饰器
- 装饰器本质就是一个高阶函数
- 通过装饰器可以在不修改原来代码的情况下,对类、属性、方法、访问器、参数进行修改
- 装饰器是可以直接修类的,但是使用中尽量遵循OCP(开闭原则)

使用装饰器
- 装饰器在TS中是一个实验性功能,需要在配置文件中开启
- "experimentalDecorators": true 用于开启TS中的装饰器, false就是使用JS的装饰器
*/

// 定义一个装饰器,装饰器本质就是一个高阶函数
// 需要更具装饰器类型的不同定义不同的参数
// 类装饰器需要一个参数,参数类型就是类
function render(target: Function) {
const oldRender = target.prototype.render;

target.prototype.render = function () {
return `<h1>${oldRender.apply(this)}</h1>`;
}
}

@render
class Person {
constructor(public name: string) { }

render() {
return this.name;
}
}

@render
class News {
constructor(public title: string) { }

render() {
return this.title;
}
}

const p = new Person("孙悟空");
const n = new News("孙悟空大闹天宫");

console.log(p.render());
console.log(n.render());

类装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/* 
类装饰器
属性装饰器
方法装饰器
访问器装饰器
参数装饰器


类装饰器
- 参数:
1.被装饰的类
- 作用:
- 修改类
- 修改方式:
1.直接修改,通过类直接对其进行修改
- 通过装饰器添加的属性无法直接访问
2.返回一个新的类,替换旧的类
- 新的类必须得是旧类的子类
*/

function render(target: Function) {
const oldRender = target.prototype.render;

target.prototype.render = function () {
return `<h1>${oldRender.apply(this)}</h1>`;
}
}

function deco(target: Function) {
target.prototype.age = 18;
}

function deco2<T extends { new(...args: any[]): any }>(target: T) {
return class extends target { }
}

@deco
class Person {
constructor(public name: string) { }

render() {
return this.name;
}
}

/*
selector <my-news />
template <h1>今天天气真不错!</h1>
*/
function Component1(target: Function) {
target.prototype.selector = "my-news";
target.prototype.template = "<h1>今天天气真不错!</h1>";
}

// 装饰器工厂,一个返回装饰器的函数
function Component2(options: { selector: string; template: string }) {
return function (target: Function) {
target.prototype.selector = options.selector;
target.prototype.template = options.template;
}
}

// @Component1
@Component2({
selector: "news",
template: "<h1>今天天气真不错!</h1>"
})
class News {
constructor(public title: string) { }
}

属性装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 
属性装饰器
- 属性装饰器定义在属性之上,不能修改属性!
- 主要用来在属性上附加一些元数据!
1.target
如果是静态属性,target是类(Class)
如果是实例属性,target是原型(Class.prototype)
2.propertyName
属性名

- 属性装饰器,无法对实例属性进行任何实质的读取和修改
在属性装饰器中,只能对原型进行一些操作
*/

function deco(target: object, propertyName: string) {
console.log(target, propertyName);
}

class Person {
@deco
name = "孙悟空";
age = 18;
}

元数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import "reflect-metadata";
export { }

/*
元数据(metadata)
- 元数据就是用来描述数据的数据

使用元数据
- 通常会使用 reflect-metadata 来处理元数据
- 这是一个对ES中原生Reflect扩展工具
- 使用步骤:
1.安装
npm i reflect-metadata
2.引入
*/

// 需要使用元数据来描述字段是否必须的
class User {
username: string;
password: string;
nickname: string;

constructor(username: string, password: string, nickname: string) {
this.username = username;
this.password = password;
this.nickname = nickname;
}
}

const u = new User("孙悟空", "123123", "大师兄");

/*
Reflect.defineMetadata
1.metadataKey(字符串或符号)
2.metadataValue(任意值)
3.对象(原型对象)
4.属性名

Reflect.getMetadata
Reflect.deleteMetadata
1.metadataKey(字符串或符号)
2.对象(原型对象)
3.属性名
*/

const reqKey = Symbol();

Reflect.defineMetadata(reqKey, true, User.prototype, "username");

const result = Reflect.getMetadata(reqKey, u, "password");
console.log(result);

function checkRequired(user: User) {
// 检查是否传递了必要的字段
for (const prop in user) {
if (prop === "username" || prop === "password") {
if (!user[prop]) {
console.log(`${prop}属性是必须的`);
}
}
}
}

属性装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import "reflect-metadata";
export { }

const reqKey = Symbol();

function required(target: any, propName: string) {
Reflect.defineMetadata(reqKey, true, target, propName);
}

/*
Reflect.metadata()
- 创建元数据的装饰器
*/

class User {
// @required
@Reflect.metadata(reqKey, true)
username: string;
@required
password: string;
nickname: string;

constructor(username: string, password: string, nickname: string) {
this.username = username;
this.password = password;
this.nickname = nickname;
}
}

const u = new User("", "", "");


function checkRequired(obj: { [key: string]: any }) {
// 检查是否传递了必要的字段
for (const prop in obj) {
if (Reflect.getMetadata(reqKey, obj, prop)) {
if (!obj[prop]) {
console.log(`${prop}是必须的`);
}
}
}
}

checkRequired(u);

方法装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/* 
方法装饰器
- 用于对方法进行扩展、修改
- 参数:
1.target 被装饰的对象
静态方法 类
实例方法 原型
2.methodName 方法名
3.descriptor 描述符
value: [Function: sayHello],
writable: true,
enumerable: false,
configurable: true

- 返回值:
返回的对象会作为新的描述符使用
*/

function deco(target: any, methodName: string, descriptor: PropertyDescriptor) {
descriptor.value = () => {
console.log("新函数");
}
}

/*
创建一个装饰器,可以在方法调用时记录日志
*/
function logger(target: any, methodName: string, descriptor: PropertyDescriptor) {
// 获取旧方法
const oldMethod: Function = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`${methodName}函数调用了 - ${new Date()}`);
oldMethod.apply(this, args)
}
}

class Person {
@deco
sayHello() {
console.log("sayHello()方法");
}

@logger
sum(a: number, b: number) {
return a + b;
}
}

const p = new Person();
// p.sayHello();
p.sum(123, 345);

访问器装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/* 
访问器装饰器
- 用于装饰访问器(getter和setter)
- 参数和方法装饰器一样
*/

function setterLog(target: any, propName: string, descriptor: PropertyDescriptor) {
const oldSet = descriptor.set;

descriptor.set = function (arg: any) {
console.log(`${propName}被修改了,在${new Date()}`);
oldSet?.call(this, arg);
}
}

class Person {
private _name: string;

constructor(name: string) {
this._name = name;
}

@setterLog
get name() {
return this._name;
}

set name(name: string) {
this._name = name;
}
}

const p = new Person("孙悟空");

p.name = "猪八戒";

console.log(p.name);

参数装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 
参数装饰器
- 用来装饰参数,为其附加一些元数据
- 参数:
1.target 被装饰的对象
2.methodName 方法名
3.parameterIndex 参数索引
*/

function deco(target: any, methodName: string, parameterIndex: number) {
console.log(target, methodName, parameterIndex);
}

class Person {
sun(@deco a: number, b: number) {
return a + b;
}
}

装饰器执行顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/* 
装饰器工厂
*/
function deco(name: string) {
console.log("工厂调用了", name);

return function (...args: any[]) {
// console.log("装饰器执行了:", name);

}
}

/*
装饰器顺序:实例 --> 静态 --> 类
*/

@deco("类装饰器")
class Person {
@deco("实例属性")
name = "孙悟空";

@deco("静态属性")
static age = 18;

@deco("实例方法")
sum(a: number, @deco("实例参数") b: number) {
return a + b;
}

@deco("静态方法")
static sayHello(@deco("静态参数") a: string) {
console.log(`Hello ${a}`);
}
}

function f(name: string) {
return function (target: any, methodName: string, desc: PropertyDescriptor) {
const oldMethod = desc.value
desc.value = function () {
console.log(`${name}开始...`);
oldMethod();
console.log(`${name}结束...`);
}
}
}

class Dog {
@f("1")
@f("2")
@f("3")
test() {
console.log("test方法...");
}
}

new Dog().test();