TypeSript笔记
之前 TS 笔记由于放在长文章后面阅读体验不太好,故单独抽离出来一篇文章。
本文主要记录了 TS 的学习知识与笔记。
基本类型
TS
有以下几种数据类型:
基础类型:包括
boolean
(布尔值)、number
(数字)、string
(字符串)、null
(空值)、undefined
(未定义值)、bigint
(大整型)和symbol
(符号)等。这些类型和JavaScript
的基本类型基本一致,只是TS
在编译时会检查变量的类型是否匹配。例如:1
2
3
4
5
6
7let isDone: boolean = false; // 声明一个布尔类型的变量 let age: number = 18; // 声明一个数字类型的变量 let name: string = "Alice"; // 声明一个字符串类型的变量 let x: null = null; // 声明一个空值类型的变量 let y: undefined = undefined; // 声明一个未定义值类型的变量 let a: bigint = 2172141653n; // 定义一个大整型变量 let z: symbol = Symbol("key"); // 声明一个符号类型的变量
数组类型:用来表示一组相同类型的数据。TS 有两种方式可以定义数组类型,一种是在元素类型后面加上
[]
,另一种是使用泛型Array<元素类型>
。例如:1
2let arr1: number[] = [1, 2, 3]; // 声明一个数字类型的数组 let arr2: Array<number> = [4, 5, 6]; // 声明一个数字类型的数组,使用泛型
元组类型:用来表示一个已知元素数量和类型的数组,各元素的类型不必相同,但是对应位置的类型需要相同。例如:
1
2
3let tuple: [string, number]; // 声明一个元组类型的变量 tuple = ["Bob", 20]; // 赋值正确,字符串和数字类型分别对应 tuple = [20, "Bob"]; // 赋值错误,类型不匹配
枚举类型:用来定义一组有名字的常数,可以方便地访问和使用。TS 支持数字枚举和字符串枚举,还可以使用
const
关键字定义常量枚举,以提高性能。例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24enum Color { Red, Green, Blue, } // 声明一个数字枚举类型 let c: Color = Color.Blue; // 赋值正确,c的值为2 console.log(c); // 输出2 enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT", } // 声明一个字符串枚举类型 let d: Direction = Direction.Left; // 赋值正确,d的值为'LEFT' console.log(d); // 输出'LEFT' const enum Month { Jan, Feb, Mar, } // 声明一个常量枚举类型 let m: Month = Month.Feb; // 赋值正确,m的值为1 console.log(m); // 输出1
any 类型:用来表示任意类型的数据,可以赋值给任何类型的变量,也可以接受任何类型的赋值。这样可以避免类型检查的错误,但是也会失去类型检查的好处。例如:
1
2
3let a: any = 1; // 声明一个任意类型的变量 a = "hello"; // 赋值正确,可以赋值为字符串类型 a = true; // 赋值正确,可以赋值为布尔类型
void 类型:用来表示没有任何类型,一般用于标识函数的返回值类型,表示该函数没有返回值。例如:
1
2
3
4function sayHello(): void { // 声明一个返回值为void类型的函数 console.log("Hello"); }
never 类型:用来表示永远不会出现的值的类型,例如抛出异常或无限循环的函数的返回值类型。例如:
1
2
3
4
5
6
7
8
9function error(message: string): never { // 声明一个返回值为never类型的函数 throw new Error(message); // 抛出异常 } function loop(): never { // 声明一个返回值为never类型的函数 while (true) {} // 无限循环 }
unknown 类型:
unknown
类型和any
类型有些相似,但是更加安全,因为它不允许对未经类型检查的值进行任何操作,除非使用类型断言或类型收缩来缩小范围。例如:1
2
3
4
5
6
7
8let u: unknown = "Hello"; // 声明一个unknown类型的变量 u = 10; // 赋值正确,可以赋值为任何类型 console.log(u + 1); // 错误,不能对unknown类型进行运算 console.log((u as number) + 1); // 正确,使用类型断言缩小范围 if (typeof u === "number") { // 正确,使用类型收缩缩小范围 console.log(u + 1); }
查看 never 类型与 void 类型的区别
TS 中的 never 类型和 void 类型是两种特殊的类型,它们的用法和含义有一些区别:
never 类型:用来表示永远不会出现的值的类型,例如抛出异常或无限循环的函数的返回值类型。
never
类型是任何类型的子类型,也就是说never
类型的值可以赋值给任何类型的变量,但是没有类型的值可以赋值给never
类型的变量(除了never
本身)。这意味着never
类型可以用来进行详尽的类型检查,避免出现不可能的情况。例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23function error(message: string): never { // 声明一个返回值为never类型的函数 throw new Error(message); // 抛出异常 } function loop(): never { // 声明一个返回值为never类型的函数 while (true) {} // 无限循环 } function check(x: string | number) { switch (typeof x) { case "string": // do something break; case "number": // do something break; default: const never: never = x; // 错误,x的类型不可能是never // do something } }
void 类型:用来表示没有任何类型,一般用于标识函数的返回值类型,表示该函数没有返回值。
void
类型的变量只能赋值为undefined
和null
(在严格模式下,只能赋值为undefined
)。void
类型的作用是避免不小心使用了空指针导致的错误,和 C 语言中的void
是类似的。例如:1
2
3
4
5
6
7
8
9function sayHello(): void { // 声明一个返回值为void类型的函数 console.log("Hello"); } let x: void = undefined; // 声明一个void类型的变量 x = null; // 赋值正确,如果不是严格模式 x = 1; // 错误,不能赋值为其他类型 console.log(x); // 输出undefined或null
查看 any 类型与 unknown 类型的区别
TS
中any
类型与unknown
类型是两种特殊的类型,它们都可以接受任何类型的值,但它们之间有一些重要的区别。下面我将从以下几个方面来讲解这两种类型的特点和用法,以及它们的异同:
定义和赋值:
any
类型是TS
中最宽泛的类型,它表示任意类型的值,可以赋值给任何类型的变量,也可以接受任何类型的值。unknown
类型是 TS 3.0 中引入的一种新的类型,它表示未知类型的值,也可以赋值给任何类型的变量,也可以接受任何类型的值。例如:1
2
3
4
5
6let a: any; // 定义一个any类型的变量a let b: unknown; // 定义一个unknown类型的变量b a = 1; // 可以给a赋值为数字 a = "hello"; // 可以给a赋值为字符串 b = 2; // 可以给b赋值为数字 b = "world"; // 可以给b赋值为字符串
操作和访问:
any
类型的变量可以进行任何操作和访问,不会有类型检查的错误,但这也会导致一些潜在的问题,比如访问不存在的属性或方法,或者调用不合法的参数。unknown
类型的变量则不能进行任何操作和访问,除非进行类型断言或类型收缩,否则会有类型检查的错误,这样可以保证类型的安全性。例如:1
2
3
4
5
6let a: any; let b: unknown; a.foo(); // 可以调用任意的方法,不会报错,但可能运行时出错 a + 1; // 可以进行任意的运算,不会报错,但可能得到意外的结果 b.foo(); // 不能调用任意的方法,会报错:Object is of type 'unknown' b + 1; // 不能进行任意的运算,会报错:Object is of type 'unknown'
赋值给其他类型:
any
类型的变量可以赋值给任何类型的变量,不会有类型检查的错误,但这也会导致一些潜在的问题,比如赋值给不兼容的类型,或者覆盖了原有的类型信息。unknown
类型的变量则只能赋值给any
类型或unknown
类型的变量,否则会有类型检查的错误,这样可以保证类型的一致性。例如:1
2
3
4
5
6
7
8let a: any; let b: unknown; let c: number; let d: string; c = a; // 可以把any类型赋值给number类型,不会报错,但可能赋值不合法的值 d = a; // 可以把any类型赋值给string类型,不会报错,但可能赋值不合法的值 c = b; // 不能把unknown类型赋值给number类型,会报错:Type 'unknown' is not assignable to type 'number' d = b; // 不能把unknown类型赋值给string类型,会报错:Type 'unknown' is not assignable to type 'string'
类型断言:类型断言是一种告诉编译器我们比它更了解类型的方式,它可以让我们强制把一个类型转换为另一个类型,但这也有一定的风险,比如断言不合法的类型,或者忽略了一些类型检查。
any
类型的变量可以使用类型断言转换为任何类型,不会有类型检查的错误,但这也会导致一些潜在的问题,比如断言错误的类型,或者丢失了类型信息。unknown
类型的变量则可以使用类型断言转换为任何类型,但这需要我们明确地指定要转换的类型,这样可以保证类型的正确性。例如:1
2
3
4
5
6let a: any; let b: unknown; let c = a as number; // 可以把any类型断言为number类型,不会报错,但可能断言错误的类型 let d = a as string; // 可以把any类型断言为string类型,不会报错,但可能断言错误的类型 let e = b as number; // 可以把unknown类型断言为number类型,不会报错,但需要明确指定类型 let f = b as string; // 可以把unknown类型断言为string类型,不会报错,但需要明确指定类型
类型收缩:类型收缩是一种让编译器自动推断出更具体的类型的方式,它可以让我们根据一些条件判断来缩小类型的范围,从而进行一些操作和访问。
any
类型的变量不能使用类型收缩,因为它已经是最宽泛的类型,没有更具体的类型可以推断出来。unknown
类型的变量则可以使用类型收缩,通过一些类型保护的方法,比如typeof
,instanceof
,in
等,来推断出更具体的类型,从而进行一些操作和访问。例如:1
2
3
4
5
6
7
8let a: any; let b: unknown; if (typeof a === "number") { a.toFixed(2); // 不能使用类型收缩,会报错:Object is of type 'any' } if (typeof b === "number") { b.toFixed(2); // 可以使用类型收缩,不会报错,推断出b是number类型 }
查看 ts 中的元组 tuple
TS 中的元组是一种特殊的数组,它可以存储不同类型的元素,并且元素的个数和类型在定义时就已经确定了。元组的语法格式如下:
1
let tuple_name: [type1, type2, ..., typeN] = [value1, value2, ..., valueN];
例如,我们可以定义一个元组,包含一个字符串和一个数字:
1
let mytuple: [string, number] = ["Hello", 42];
元组的元素可以通过索引来访问,索引从 0 开始,例如:
1
2
console.log(mytuple[0]); // 输出 "Hello"
console.log(mytuple[1]); // 输出 42
元组的长度和类型都是固定的,所以不能越界访问或修改元素,也不能添加或删除超出范围的元素。否则,TS 编译器会报错。但是,我们可以对元组的元素进行更新操作,例如:
1
2
mytuple[0] = "World"; // 更新第一个元素
console.log(mytuple[0]); // 输出 "World"
元组还有一些与其相关的方法,主要有以下几种:
push()
:向元组的末尾添加一个新元素,返回新的长度。注意,这个方法会改变原来的元组,而且添加的元素必须是元组中已有类型的联合类型。pop()
:从元组的末尾移除一个元素,返回被移除的元素。注意,这个方法会改变原来的元组。concat()
:连接两个元组,返回一个新的元组。注意,这个方法不会改变原来的元组,而且连接后的元组的类型必须是两个元组的类型的联合类型。slice()
:从元组中截取一部分元素,返回一个新的元组。注意,这个方法不会改变原来的元组,而且截取后的元组的类型必须是原来元组的类型的联合类型。
下面是一些使用这些方法的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let mytuple: [string, number] = ["Hello", 42];
let yourtuple: [string, number] = ["true", 100];
mytuple.push("World"); // 添加一个字符串元素
console.log(mytuple); // 输出 ["Hello", 42, "World"]
let last = mytuple.pop(); // 移除最后一个元素
console.log(last); // 输出 "World"
console.log(mytuple); // 输出 ["Hello", 42]
let newtuple = mytuple.concat(yourtuple); // 连接两个元组
console.log(newtuple); // 输出 ["Hello", 42, 'true', 100]
let subtuple = newtuple.slice(1, 3); // 截取一部分元组
console.log(subtuple); // 输出 [42, 'true']
枚举类型
枚举类型是一种在 TypeScript
中定义一组带名字的常量的方式。枚举可以清晰地表达意图或创建一组有区别的用例。TypeScript
支持基于数字和基于字符串的枚举。
1
2
3
4
5
6
7
enum Direction {
Up,
Down = 10,
Left,
Right,
}
console.log(Direction.Up, Direction.Down, Direction.Left, Direction.Right); // 0 10 11 12
数字枚举:每个成员都有一个数字值,可以是常量或计算出来的。如果没有初始化器,第一个成员的值为 0,后面的成员依次递增。例如:
1
2
3
4
5
6enum Direction { Up, // 0 Down, // 1 Left, // 2 Right, // 3 }
字符串枚举:每个成员都必须用字符串字面量或另一个字符串枚举成员初始化。字符串枚举没有自增长的行为,但可以提供一个运行时有意义的并且可读的值。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT", } console.log(Direction["Right"], Direction.Up); // RIGHT UP // 后续也需要设置字符串 enum Direction { Up = "UP", Down, // error TS1061: Enum member must have initializer Left, // error TS1061: Enum member must have initializer Right, // error TS1061: Enum member must have initializer }
异构枚举:可以混合字符串和数字成员,但不建议这么做。例如:
1
2
3
4enum BooleanLikeHeterogeneousEnum { No = 0, Yes = "YES", }
常量枚举:使用
const enum
关键字定义,只能使用常量枚举表达式初始化成员,不能包含计算或动态的值。常量枚举在编译阶段会被删除,不会生成任何代码。例如:1
2
3
4
5
6
7
8
9
10
11
12
13const enum Direction { Up, Down, Left, Right, } let directions = [ Direction.Up, Direction.Down, Direction.Left, Direction.Right, ]; // 编译后变为 [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */]
联合枚举和枚举成员类型:当所有枚举成员都是字面量类型时(不带有初始值或者初始化为字符串或数字字面量),枚举成员本身就是类型,而枚举类型本身就是每个成员的联合类型。这样可以实现更精确的类型检查和约束。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19enum ShapeKind { Circle, Square, } interface Circle { kind: ShapeKind.Circle; // 只能是 ShapeKind.Circle 类型 radius: number; } interface Square { kind: ShapeKind.Square; // 只能是 ShapeKind.Square 类型 sideLength: number; } let c: Circle = { kind: ShapeKind.Square, // Error! 类型不匹配 radius: 100, };
运行时的枚举:枚举是在运行时真正存在的对象,可以作为参数传递给函数或从函数返回。例如:
1
2
3
4
5
6
7
8
9
10
11
12enum E { X, Y, Z, } function f(obj: { X: number }) { return obj.X; } // Works, since 'E' has a property named 'X' which is a number. f(E);
反向映射:数字枚举成员具有反向映射,可以根据枚举值得到对应的名字。例如:
1
2
3
4
5
6enum Enum { A, } let a = Enum.A; let nameOfA = Enum[a]; // "A"
查看枚举的本质
一个枚举的案例如下
1
2
3
4
5
6
7
8
enum Direction {
Up,
Down,
Left,
Right,
}
console.log(Direction.Up === 0); // true
console.log(Direction[0], typeof Direction[0]); // Up string
编译后的js
代码
1
2
3
4
5
6
7
8
9
10
11
12
var Direction;
(function (Direction) {
Direction[(Direction["Up"] = 0)] = "Up";
Direction[(Direction["Down"] = 1)] = "Down";
Direction[(Direction["Left"] = 2)] = "Left";
Direction[(Direction["Right"] = 3)] = "Right";
})(Direction || (Direction = {}));
(function (Direction) {
Direction[(Direction["Center"] = 1)] = "Center";
})(Direction || (Direction = {}));
console.log(Direction.Up === 0); // true
console.log(Direction[0], typeof Direction[0]); // Up string
接口
ts
中的接口是一种用来描述对象的形状(shape
)的语法,它可以规定对象的属性和方法,以及它们的类型。接口可以让我们在编写代码时进行类型检查,避免出现类型错误。接口也可以提高代码的可读性和可维护性,让我们更清楚地知道对象的结构和功能。
ts 中的接口有以下几个特点:
可选属性:接口中的属性可以用
?
标记为可选,表示这个属性可以存在也可以不存在。例如:1
2
3
4
5
6
7
8interface Person { name: string; // 必须属性 age?: number; // 可选属性 } let p1: Person = { name: "Alice" }; // 合法,age可以省略 let p2: Person = { name: "Bob", age: 18 }; // 合法,age可以存在 let p3: Person = { name: "Charlie", gender: "male" }; // 非法,gender不是接口定义的属性
只读属性:接口中的属性可以用
readonly
标记为只读,表示这个属性只能在对象创建时赋值,不能再修改。例如:1
2
3
4
5
6
7interface Point { readonly x: number; // 只读属性 readonly y: number; // 只读属性 } let p1: Point = { x: 10, y: 20 }; // 合法,创建时赋值 p1.x = 30; // 非法,不能修改只读属性
函数类型:接口中可以定义函数的类型,即参数列表和返回值类型。例如:
1
2
3
4
5
6
7
8
9
10interface SearchFunc { (source: string, subString: string): boolean; // 函数类型 } let mySearch: SearchFunc; // 定义一个变量符合函数类型 mySearch = function (src, sub) { // 实现一个函数符合函数类型 let result = src.search(sub); return result > -1; };
索引类型:接口中可以定义索引的类型,即通过
[]
访问对象的类型。索引可以是数字或字符串。例如:1
2
3
4
5
6
7
8interface StringArray { [index: number]: string; // 索引类型 } let myArray: StringArray; // 定义一个变量符合索引类型 myArray = ["Bob", "Fred"]; // 赋值一个数组符合索引类型 let myStr: string = myArray[0]; // 访问数组元素符合索引类型
类类型:接口中可以定义类的类型,即类的构造函数和实例方法。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17interface ClockInterface { currentTime: Date; // 实例属性 setTime(d: Date): void; // 实例方法 } class Clock implements ClockInterface { // 类实现接口 currentTime: Date; constructor(h: number, m: number) { this.currentTime = new Date(); this.currentTime.setHours(h); this.currentTime.setMinutes(m); } setTime(d: Date) { this.currentTime = d; } }
继承接口:接口之间可以相互继承,从而拥有父接口的属性和方法。一个接口也可以继承多个接口,实现多重继承。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17interface Shape { color: string; // 父接口属性 } interface PenStroke { penWidth: number; // 父接口属性 } interface Square extends Shape, PenStroke { // 子接口继承两个父接口 sideLength: number; // 子接口属性 } let square = {} as Square; // 定义一个变量符合子接口类型 square.color = "blue"; // 赋值父接口Shape的属性 square.sideLength = 10; // 赋值子接口Square的属性 square.penWidth = 5.0; // 赋值父接口PenStroke的属性
类(class)
TS
中的类的基本使用与TS
相同,只不过引入了类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Car {
// 字段
engine: string;
// 构造函数
constructor(engine: string) {
this.engine = engine;
}
// 方法
disp(): void {
console.log("发动机为 : " + this.engine);
}
}
但TS
相对于JS
中的class
也添加了一些更高阶的类的特性。
访问修饰符
TS
引入访问修饰符,类似JAVA
。值得注意的是,访问修饰符的特性是TS
本身的规范,JS
并不能实现类似访问修饰符的特性。
public
:公开,可以自由的访问类程序里定义的成员。private
:私有,只能在类的内部进行访问。protected
:受保护,除了在该类的内部可以访问,还可以在子类中仍然可以访问。
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
class Animal {
public name: string; // 公开的,可以在任何地方访问
private age: number; // 私有的,只能在类的内部访问
protected color: string; // 受保护的,可以在类的内部和子类中访问
readonly species: string; // 只读的,只能在声明时或构造函数中赋值
constructor(name: string, age: number, color: string, species: string) {
this.name = name;
this.age = age;
this.color = color;
this.species = species;
}
// 访问器,用来获取或设置私有或受保护的成员
get Age() {
return this.age;
}
set Age(value: number) {
if (value > 0) {
this.age = value;
}
}
get Color() {
return this.color;
}
set Color(value: string) {
this.color = value;
}
}
class Cat extends Animal {
constructor(name: string, age: number, color: string) {
super(name, age, color, "cat"); // 调用父类的构造函数
}
// 重写父类的方法
get Color() {
return "The color of this cat is " + super.Color; // 访问父类的受保护成员
}
}
let animal = new Animal("Tom", 3, "black", "dog");
console.log(animal.name); // 可以访问公开成员
// console.log(animal.age); // 错误,不能访问私有成员
console.log(animal.Age); // 可以通过访问器访问私有成员
// console.log(animal.color); // 错误,不能访问受保护成员
console.log(animal.Color); // 可以通过访问器访问受保护成员
console.log(animal.species); // 可以访问只读成员
// animal.species = "bird"; // 错误,不能修改只读成员
let cat = new Cat("Jerry", 2, "white");
console.log(cat.name); // 可以访问公开成员
// console.log(cat.age); // 错误,不能访问私有成员
console.log(cat.Age); // 可以通过访问器访问私有成员
// console.log(cat.color); // 错误,不能访问受保护成员
console.log(cat.Color); // 可以通过访问器访问受保护成员,注意这里调用的是子类重写的方法
console.log(cat.species); // 可以访问只读成员
// cat.species = "mouse"; // 错误,不能修改只读成员
TS
还引入了属性访问修饰符readonly
。注意:readonly
只能用于修饰属性,可以配合class
的classfield
写法例如:
1
2
3
4
5
6
7
8
9
10
11
class Person {
public readonly name: string; // 公有的只读属性
private readonly age: number; // 私有的只读属性
protected readonly gender: string; // 受保护的只读属性
constructor(name: string, age: number, gender: string) {
this.name = name;
this.age = age;
this.gender = gender;
}
}
抽象类
TS
中的抽象类是一种特殊的类,它不能被直接实例化,只能作为其他类的基类来提供通用的属性和方法的定义。抽象类主要用于定义一组相关的类的共同结构和行为,以及强制子类实现特定的方法。抽象类用 abstract
关键字修饰,抽象类中的抽象方法也用 abstract
关键字修饰,抽象方法没有具体的实现,只有声明。抽象类可以有构造器,也可以有非抽象的属性和方法。抽象类的子类必须实现抽象类中的所有抽象方法,否则也会成为抽象类。
以下是一个 TS
中抽象类的代码示例:
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
// 定义一个抽象类Animal,它有一个name属性,一个构造器,一个sayHello方法和一个抽象的eat方法
abstract class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
sayHello(): void {
console.log(`Hello, I am ${this.name}`);
}
abstract eat(): void; // 抽象方法,没有实现
}
// 定义一个子类Dog,它继承了Animal类,它必须实现Animal类中的抽象方法eat
class Dog extends Animal {
constructor(name: string) {
super(name); // 调用父类的构造器
}
eat(): void {
// 实现抽象方法
console.log(`${this.name} is eating bones`);
}
}
// 定义一个子类Cat,它继承了Animal类,它必须实现Animal类中的抽象方法eat
class Cat extends Animal {
constructor(name: string) {
super(name); // 调用父类的构造器
}
eat(): void {
// 实现抽象方法
console.log(`${this.name} is eating fish`);
}
}
// 创建一个Dog对象和一个Cat对象
let dog = new Dog("Tommy");
let cat = new Cat("Kitty");
// 调用它们的方法
dog.sayHello(); // Hello, I am Tommy
dog.eat(); // Tommy is eating bones
cat.sayHello(); // Hello, I am Kitty
cat.eat(); // Kitty is eating fish
// 不能创建一个Animal对象,因为Animal是抽象类
let animal = new Animal("Jack"); // 编译错误,不能实例化抽象类
函数
ts
类型定义1
2
3
4
5
6
7// 方式一,函数类型的对象字面量,可用于函数重载 type LongHand = { (a: number): number; }; // 方式二,函数类型的别名,只能定义一个函数的类型,而不能定义多个函数的类型,不可用于函数重载 type ShortHand = (a: number) => number;
重载签名的作用是为了让 TS 编译器能够正确地推断函数的参数类型和返回类型,从而提供更好的类型检查和代码提示。如果没有重载签名,函数依旧能够发挥作用,但是 TS 编译器可能无法识别函数的参数类型和返回类型,导致类型错误或警告。例如,如果你在 TS 中使用以下的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// 没有重载签名的函数 function add(x: any, y: any): any { // 函数体 if (typeof x === "string" && typeof y === "string") { // 字符串相拼接 return x + y; } else if (typeof x === "number" && typeof y === "number") { // 数字相加 return x + y; } else { // 抛出错误 throw new Error("Invalid arguments"); } } // 调用函数 let a = add("Hello", "World"); // a的类型是any let b = add(1, 2); // b的类型是any let c = add("1", 2); // c的类型是any,但是会抛出错误
你会发现,变量 a、b 和 c 的类型都是 any,这意味着 TS 编译器无法确定它们的具体类型,也就无法提供类型检查和代码提示。例如,如果你想对 a 进行字符串操作,或者对 b 进行数学运算,TS 编译器可能会提示你这样做是不安全的,因为它们可能不是你期望的类型。而如果你使用重载签名,TS 编译器就能够根据你传入的参数类型,推断出函数的返回类型,从而提供更好的类型检查和代码提示。例如,如果你在 TS 中使用以下的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// 有重载签名的函数 function add(x: string, y: string): string; function add(x: number, y: number): number; function add(x: any, y: any): any { // 函数体 if (typeof x === "string" && typeof y === "string") { // 字符串相拼接 return x + y; } else if (typeof x === "number" && typeof y === "number") { // 数字相加 return x + y; } else { // 抛出错误 throw new Error("Invalid arguments"); } } // 调用函数 let a = add("Hello", "World"); // a的类型是string let b = add(1, 2); // b的类型是number let c = add("1", 2); // c的类型是any,但是会抛出错误
你会发现,变量 a 的类型是 string,变量 b 的类型是 number,这意味着 TS 编译器能够确定它们的具体类型,也就能够提供类型检查和代码提示。例如,如果你想对 a 进行字符串操作,或者对 b 进行数学运算,TS 编译器就不会提示你这样做是不安全的,因为它们是你期望的类型。而变量 c 的类型仍然是 any,因为你传入了不匹配的参数类型,这时候 TS 编译器会提示你函数的重载签名没有匹配到你的参数组合,也就提醒你可能会出现错误。
可选参数与默认参数
可选参数必须跟在必须参数后面的例子:
1
2
3
4
5
6
7function greet(name: string, greeting?: string) { // name是必须参数,greeting是可选参数 console.log(greeting ? `${greeting}, ${name}!` : `Hello, ${name}!`); } greet("Alice"); // Hello, Alice! greet("Bob", "Hi"); // Hi, Bob!
有默认值的参数可以放在必须参数的前面或后面的例子:
1
2
3
4
5
6
7
8function add(x: number = 0, y: number) { // x是有默认值的参数,y是必须参数 return x + y; } console.log(add(1, 2)); // 3 console.log(add(undefined, 3)); // 3 console.log(add(4)); // 报错,缺少必须参数y
有默认值的参数如果放在必须参数的后面,那么这样函数的签名和可选参数就是一样的:
1
2
3
4
5
6
7
8function multiply(x: number, y: number = 1) { // x是必须参数,y是有默认值的参数 return x * y; } console.log(multiply(2, 3)); // 6 console.log(multiply(4)); // 4 console.log(multiply(5, undefined)); // 5
这个函数的签名和
function multiply(x: number, y?: number)
是一样的。
泛型
泛型是一种编程范式,它允许在程序中定义形式类型参数,然后在泛型实例化时使用实际类型参数来替换形式类型参数。泛型可以提高代码的复用性和类型安全性,让程序更灵活和通用。
TS
中的泛型有以下几种应用场景:
泛型函数:可以定义一个函数,它的参数和返回值的类型由一个类型变量来表示,这样就可以适用于不同的类型,而不需要重复编写相同逻辑的函数。例如:
1
2
3function identity<T>(arg: T): T { return arg; }
这个函数可以接受任何类型的参数,并返回相同类型的值。我们可以在调用时指定泛型参数的实际类型,如
identity<string>("hello")
,或者让TS
自动推断类型,如identity(42)
。泛型接口:可以定义一个接口,它的属性或方法的类型由一个或多个类型变量来表示,这样就可以定义通用的数据结构或契约。例如:
1
2
3
4interface MyArray<T> extends Array<T> { first: T | undefined; last: T | undefined; }
这个接口继承了数组接口,并添加了两个属性,它们的类型都是泛型参数
T
。我们可以实现这个接口,并指定T
的实际类型,如class StringArray implements MyArray<string>
。泛型类:可以定义一个类,它的属性或方法的类型由一个或多个类型变量来表示,这样就可以创建通用的类。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class GetMin<T> { arr: T[] = []; add(ele: T) { this.arr.push(ele); } min(): T { let min = this.arr[0]; this.arr.forEach(function (value) { if (value < min) { min = value; } }); return min; } }
这个类可以创建一个存储任何类型元素的数组,并提供一个方法返回最小值。我们可以创建这个类的实例,并指定
T
的实际类型,如let gm1 = new GetMin<number>()
。泛型约束:可以使用
extends
关键字来限制泛型参数的范围,使之只能是某个类型或其子类型。这样就可以在泛型中使用一些特定的属性或方法。例如:1
2
3
4
5
6
7
8interface Point { x: number; y: number; } function toArray<T extends Point>(a: T, b: T): T[] { return [a, b]; }
这个函数只能接受
Point
或其子类型作为参数,并返回相同类型的数组。我们不能传入其他类型的参数,如toArray(1, 2)
会报错。
高级类型
当然可以,我会给你一些 TS 中高级类型的例子,你可以参考一下:
交叉类型(Intersection Types)的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// 定义两个接口 interface A { name: string; age: number; } interface B { gender: "male" | "female"; hobby: string; } // 使用交叉类型将两个接口合并为一个类型 type C = A & B; // 创建一个C类型的对象 let c: C = { name: "Tom", age: 20, gender: "male", hobby: "basketball", };
联合类型(Union Types)的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 定义一个联合类型,表示一个值可以是number或string type D = number | string; // 使用联合类型作为函数参数的类型 function print(d: D) { // 使用typeof类型保护来判断d的具体类型 if (typeof d === "number") { console.log("The number is " + d); } else if (typeof d === "string") { console.log("The string is " + d); } } // 调用函数,传入不同类型的值 print(10); // The number is 10 print("Hello"); // The string is Hello
字面量类型(Literal Types)的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 定义一个字符串字面量类型,表示一个值只能是'hello'或'world' type E = "hello" | "world"; // 定义一个数字字面量类型,表示一个值只能是1或2 type F = 1 | 2; // 定义一个布尔字面量类型,表示一个值只能是true type G = true; // 定义一个模板字面量类型,表示一个值只能是'Hello ${string}' type H = `Hello ${string}`; // 使用字面量类型创建变量 let e: E = "hello"; // OK let f: F = 2; // OK let g: G = true; // OK let h: H = `Hello world`; // OK
索引类型(Indexed Types)的例子:
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// 定义一个接口 interface I { name: string; age: number; gender: "male" | "female"; } // 使用索引类型查询操作符得到I的所有属性名的类型 type J = keyof I; // J = 'name' | 'age' | 'gender' // 使用索引访问操作符得到I的某个属性的类型 type K = I["name"]; // K = string // 使用泛型约束和索引类型实现一个获取对象属性值的函数 function getProp<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } // 创建一个I类型的对象 let i: I = { name: "Alice", age: 18, gender: "female", }; // 调用函数,传入对象和属性名 let name = getProp(i, "name"); // name的类型是string,值是'Alice' let age = getProp(i, "age"); // age的类型是number,值是18
映射类型(Mapped Types)的例子:
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// 定义一个接口 interface L { name: string; age: number; gender: "male" | "female"; } // 使用映射类型将L的所有属性变为可选的 type M = { [P in keyof L]?: L[P]; }; // 使用映射类型将L的所有属性变为只读的 type N = { readonly [P in keyof L]: L[P]; }; // 使用内置的映射类型Pick从L中选择部分属性组成一个新的类型 type O = Pick<L, "name" | "gender">; // 使用映射类型创建变量 let m: M = { name: "Bob", }; // OK,只有name属性,其他属性可选 let n: N = { name: "Bob", age: 20, gender: "male", }; // OK,所有属性只读 n.name = "Tom"; // Error,不能修改只读属性 let o: O = { name: "Bob", gender: "male", }; // OK,只有name和gender属性,其他属性不存在
条件类型(Conditional Types):
1
T extends U ? X : Y
上面的意思就是,如果 T 是 U 的子集,就是类型 X,否则为类型 Y
类型别名(Type Aliases)
类型别名是一种给一个类型起一个新的名字的方式,它可以让你更方便地引用这个类型,或者给这个类型添加一些语义。类型别名使用
type
关键字来定义,例如type Name = string
表示给string
类型起了一个别名叫Name
,之后你就可以用Name
来代替string
了。其次,你需要知道什么是泛型(Generics)。泛型是一种在定义类型时使用一个或多个类型参数的方式,它可以让你创建一些适用于任意类型的通用类型,而不是限定于某个具体的类型。泛型使用
<T>
这样的语法来表示类型参数,其中T
是一个占位符,可以用任何合法的标识符来替换。例如Array<T>
表示一个泛型数组类型,它可以存放任意类型的元素,例如Array<number>
表示一个数字数组,Array<string>
表示一个字符串数组。那么,类型别名可以是泛型吗?答案是肯定的,类型别名可以使用泛型来定义一些通用的类型,这样就可以让类型别名更灵活和复用。例如,你给出的这个类型别名:
1
type Container<T> = { value: T };
它就是一个泛型类型别名,它表示一个容器类型,它有一个属性叫
value
,这个属性的类型是由类型参数T
决定的。这样,你就可以用这个类型别名来创建不同类型的容器,例如:1
2
3
4
5
6
7
8// 创建一个容器,它的value属性是一个数字 let numContainer: Container<number> = { value: 10 }; // 创建一个容器,它的value属性是一个字符串 let strContainer: Container<string> = { value: "Hello" }; // 创建一个容器,它的value属性是一个布尔值 let boolContainer: Container<boolean> = { value: true };
这些容器都是使用同一个类型别名
Container<T>
来定义的,只是类型参数T
不同而已。这样,你就可以用一个类型别名来表示多种可能性,而不需要为每种情况都定义一个新的类型。查看 keyof 与 infer 关键字的介绍
infer
是TypeScript 2.8
中引入的一个关键字,它可以用在条件类型的extends
子句中,用来推断某个类型变量的具体类型。infer
的作用是在真实分支中引用此推断类型变量,从而获取待推断的类型。infer
可以用在多种场合,例如:推断函数的参数和返回值类型
1
2
3
4
5
6
7
8
9
10
11// 定义一个类型,用来获取函数的参数类型 type ParamType<T> = T extends (...args: infer P) => any ? P : never; // 定义一个类型,用来获取函数的返回值类型 type ReturnType<T> = T extends (...args: any) => infer R ? R : never; // 测试 type Fn = (a: number, b: string) => number; type P = ParamType<Fn>; // [number, string] type R = ReturnType<Fn>; // number
推断对象的属性类型
1
2
3
4
5
6
7
8// 定义一个类型,用来获取对象的属性类型 type PropType<T, K extends keyof T> = T[K] extends infer U ? U : never; // 测试 type Obj = { a: string; b: number }; type A = PropType<Obj, "a">; // string type B = PropType<Obj, "b">; // number
推断联合类型的成员类型
1
2
3
4
5// 定义一个类型,用来获取联合类型的成员类型 type UnionType<T> = T extends infer U ? U : never; // 测试 type U = UnionType<string | number>; // string | number
keyof
是TypeScript
中的一个关键字,它可以从一个对象类型中提取出它的所有属性名,组成一个字符串或数字的字面量联合类型。例如:1
2
3
4
5
6
7type Person = { name: string; age: number; gender: string; }; type P = keyof Person; // "name" | "age" | "gender"
keyof
的作用是可以让我们在类型层面上操作对象的属性,比如限制访问对象的某些属性,或者映射对象的属性类型。keyof
也可以和泛型、索引类型、映射类型等一起使用,实现更多的类型操作。下面是一些keyof
的常见用法:使用
[]
访问对象属性的类型:1
2
3
4
5
6
7
8
9type Person = { name: string; age: number; gender: string; }; type Name = Person["name"]; // string type Age = Person["age"]; // number type Gender = Person["gender"]; // string
使用
extends
限制泛型参数的属性:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16type Person = { name: string; age: number; gender: string; }; function getProperty<T, K extends keyof T>(obj: T, key: K) { return obj[key]; } let p: Person = { name: "Alice", age: 20, gender: "female" }; let name = getProperty(p, "name"); // string let age = getProperty(p, "age"); // number let gender = getProperty(p, "gender"); // string // let hobby = getProperty(p, "hobby"); // Error: Type '"hobby"' is not assignable to type '"name" | "age" | "gender"'
使用
in
遍历对象的属性:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18type Person = { name: string; age: number; gender: string; }; type Keys = keyof Person; // "name" | "age" | "gender" type PersonRecord = { [P in Keys]: string; }; // 等价于 // type PersonRecord = { // name: string; // age: string; // gender: string; // };