前言
TS 中的高级类型有很多:class类、类型兼容型、交叉类型、泛型和keyof、索引签名类型和索引查询类型、映射类型
一、class 类
TypeScript 全面支持 ES2015 中引入的 class 关键字,并为其添加了类型注解和其他语法(比如,可见性修饰符等)
1、class 基本使用,如下
1 | // 定义 class 类 |
2 | class Person {} |
3 |
4 | // 创建实例,通过 ts 的类型推论得出 const p: Person |
5 | const p = new Person() |
解释:
· 根据 TS 中的类型推论,可以知道Person类的实例对象p的类型是Person。
· TS 中的 class,不仅提供了 class 的语法功能,也作为一种类型存在。
实例属性初始化:
1 | // 实例属性初始化,能省则省 |
2 | class Person { |
3 | age: number |
4 | gender = '男' // gender: string = '男' |
5 | } |
6 |
7 | // 创建实例,通过 ts 的类型推论得出 const p: Person |
8 | const p = new Person() |
9 |
10 | p.age // (property) age: number |
11 | p.gender // (property) gender: string |
解释:
· 声明成员 age,类型为 number(没有初始值)。
· 声明成员 gender,并设置初始值,此时,可省略类型注解(TS类型推论为string类型)。
2、构造函数
1 | class Person { |
2 | age: number |
3 | gender: number |
4 |
5 | // 构造函数不需要返回值类型 |
6 | constructor(age: number, gender: number) { |
7 | this .age = age |
8 | this .gender = gender |
9 | } |
10 | } |
11 | const p = new Person(28, '男' ) |
12 |
13 | console.log(p.age, p.gender) // 28 男 |
解释:
· 成员初始化(比如,age:number)后,才可以通过this.age 来访问实例成员。
· 需要为构造函数指定类型注解,否则会被隐式推断为 any;构造函数不需要返回值类型。
3、实例方法
1 | class Point { |
2 | x = 10 |
3 | y = 10 |
4 |
5 | scale(n: number): void { |
6 | this .x *= n |
7 | this .y *= n |
8 | } |
9 | } |
10 |
11 | const p = new Point() |
12 |
13 | p.scale(10) |
14 |
15 | console.log(p.x, p.y) // 10 20 |
解释:方法的类型注解(参数和返回值)与函数用法相同。
3、类的继承
类继承的两种方式:extends(继承父类)、implements(实现接口),JS 中只有extends,而implements 是 TS 提供的。
extends(继承父类)
1 | // 父类 |
2 | class Animal { |
3 | move() { |
4 | console.log( '走两步' ) |
5 | } |
6 | } |
7 |
8 | // 子类 |
9 | class Dog extends Animal { |
10 | name = '二哈' |
11 |
12 | bark() { |
13 | console.log( '旺旺!' ) |
14 | } |
15 | } |
16 |
17 | const d = new Dog() |
18 | d.move() // 走两步 |
19 | d.bark() // 旺旺! |
20 | console.log(d.name) // 二哈 |
解释:
· 通过 extends 关键字实现继承。
· 子类 Dog 继承父类 Animal,则 Dog 的实例对象 dog 就同时具有了父类Animal和 子类 Dog 的所有属性和方法。
implements(实现接口)
1 | interface Singale { |
2 | name: string |
3 | sing(): void |
4 | } |
5 | // 如果缺少 name 属性或 sing 方法,就会报错 |
6 | class Person implements Singale { |
7 | name = 'Jack' |
8 | sing() { |
9 | console.log( '你是我的小呀小苹果' ) |
10 | } |
11 | } |
解释:
· 通过 implements 关键字让 class 实现接囗。
· Person 类实现接口 Singable 意味着,Person类中必须提供Singable 接口中指定的所有方法和属性
4、类的可见性修饰符
类成员可见性:可以使用 TS 来控制 class 的方法或属性对于 class 外的代码是否可见。
可见性修饰符包括:public(公有的)、protected(受保护的)、private(私有的)。
public:表示公有的、公开的,公有成员可以被任何地方访问,默认可见性。
1 | // 父类 |
2 | class Animal { |
3 | public move() { |
4 | console.log( '走两步' ) |
5 | } |
6 | } |
7 |
8 | const a = new Animal() |
9 | a.move() // 走两步 |
10 |
11 | // 子类 |
12 | class Dog extends Animal { |
13 | bark() { |
14 | console.log( '旺旺!' ) |
15 | } |
16 | } |
17 |
18 | const d = new Dog() |
19 | d.move() // 走两步 |
解释:
· 在类属性或方法前面添加 public 关键字,来修饰该属性或方法是共有的。
· 因为 public 是默认可见性,所以,可以直接省略。
protected:表示受保护的,仅对其声明所在类和子类中(非实例对象)可见
1 | // 父类 |
2 | class Animal { |
3 | // 受保护的,只能在当前类或子类的其它方法中调用 |
4 | protected move() { |
5 | console.log( '走两步' ) |
6 | } |
7 | run() { |
8 | this .move() // 可以调用 |
9 | } |
10 | } |
11 |
12 | const a = new Animal() |
13 | a.move() // 不能调用 move 方法 |
14 |
15 | // 子类 |
16 | class Dog extends Animal { |
17 | bark() { |
18 | this .move() // 可以调用 |
19 | console.log( '旺旺!' ) |
20 | } |
21 |
22 | } |
23 |
24 | const d = new Dog() |
25 | d.move() // 不能调用 move 方法 |
解释:
· 在类属性或方法前面添加 protected 关键字,来修饰该属性或方法是受保护的。
· 在子类的方法内部可以通过 this 来访问父类中受保护的成员,但是,对实例不可见!
private:表示私有的,只在当前类中可见,对实例对象以及子类也是不可见的。
1 | // 父类 |
2 | class Animal { |
3 | // 当前类私有的,只能在当前类方法中调用 |
4 | private move() { |
5 | console.log( '走两步' ) |
6 | } |
7 | run() { |
8 | this .move() // 可以调用 |
9 | } |
10 | } |
11 |
12 | const a = new Animal() |
13 | a.move() // 不能调用 move 方法 |
14 |
15 | // 子类 |
16 | class Dog extends Animal { |
17 | bark() { |
18 | this .move() // 不能调用 move 方法 |
19 | console.log( '旺旺!' ) |
20 | } |
21 |
22 | } |
23 |
24 | const d = new Dog() |
25 | d.move() // 不能调用 move 方法 |
解释:
· 在类属性或方法前面添加 private 关键字,来修饰该属性或方法是私有的。
· 私有的属性或方法只在当前类中可见,对子类和实例对象也都是不可见的!
readonly(只读修饰符)除了可见性修饰符之外的常见修饰符。
readonly:表示只读,用来防止在构造函数之外对属性进行赋值。
1 | class Person { |
2 | // 只读属性 |
3 | readonly age: number = 19 |
4 |
5 | constructor(age: number) { |
6 | this .age = age // 只能在构造函数赋值 |
7 | } |
8 |
9 | setAge() { |
10 | this .age = 20 // 无法为“age”赋值,因为它是只读属性 |
11 | } |
12 |
13 | // 错误演示,readonly 不能修饰方法 |
14 | // readonly setAge() { |
15 | // this.age = 20 |
16 | // } |
17 | } |
解释:
· 使用 readonly 关键字修饰该属性是只读的,注意只能修饰属性不能修饰方法。
· 注意:属性 age 后面的类型注解(比如,此处的 number)如果不加,则 age 的类型为 18 (字面量类型)。
1 | class Person { |
2 | // 只读属性 |
3 | // 注意:只要是 readonly 来修饰的属性,必须手动添加明确的类型 |
4 | readonly age = 19 // (property) Person.age: 19 |
5 |
6 | constructor(age: number) { |
7 | this .age = age // 不能将类型“number”分配给类型“19”。 |
8 | } |
9 | } |
· 接口或者 {} 表示的对象类型,也可以使用 readonly。
1 | // 接口写法 |
2 | // interface MyPersn { |
3 | // readonly name: string |
4 | // } |
5 |
6 | // let obj: MyPersn = { |
7 | // name: 'jack' |
8 | // } |
9 |
10 | // 不使用接口指定类型 |
11 | let obj: {readonly name: string} = { |
12 | name: 'jack' |
13 | } |
14 |
15 | obj.name = 'rose' // 无法为“name”赋值,因为它是只读属性。 |
二、类型兼容性
两种类型系统:StructuralTypeSystem(结构化类型系统)、NominalTypeSystem(标明类型系统)
TS 采用的是结构化类型系统,也叫做 ducktyping(鸭子类型),类型检查关注的是值所具有的形状。
也就是说,在结构类型系统中,如果两个对象具有相同的形状,则认为它们属于同一类型
1 |
解释:
· Point和 Point2D 是两个名称不同的类。
· 变量 p 的类型被显示标注为 Point 类型,但是,它的值却是 Point2D 的实例,并且没有类型错误。
· 因为 TS 是结构化类型系统,只检查 Point 和 Point2D 的结构是否相同(相同,都具有x和y两个属性,属性类型也相同)了。
· 但是,如果在 NominalType System 中(比如,C#、Java等),它们是不同的类,类型无法兼容。
1 | // 演示类型兼容性 |
2 | let arr = [ 'a' , 'b' , 'c' ] |
3 |
4 | // forEach的回调函数,因为类型兼容型的存在,在 TS 中下面写法都是可以的 |
5 | arr.forEach(item => {}) |
6 | arr.forEach((item, index) => {}) |
7 | arr.forEach((item, index, array) => {}) |
8 |
9 | // 两个类的兼容性演示 |
10 | class Point { |
11 | x: number |
12 | y: number |
13 | } |
14 | class Point2D { |
15 | x: number |
16 | y: number |
17 | } |
18 | // 因为 TS 是结构化类型系统,只检查 Point 和 Point2D 的结构是否相同(相同,都具有x和y两个属性,属性类型也相同)了。 |
19 | const p: Point = new Point2D() |
1、对象之间的类型兼容性
注意:在结构化类型系统中,如果两个对象具有相同的形状,则认为它们属于同一类型,这种说法并不准确。
更准确的说法:对于对象类型来说,v的成员至少与x相同,则x兼容v(成员多的可以赋值给少的)。
1 | // 对象之间的类型兼容性 |
2 | class Point { |
3 | x: number |
4 | y: number |
5 | } |
6 |
7 | class Point2D { |
8 | x: number |
9 | y: number |
10 | z: number |
11 | } |
12 |
13 | const p1: Point = new Point2D() |
14 | // 错误演示 |
15 | // const p2: Point2D = new Point() // 类型 "Point" 中缺少属性 "z",但类型 "Point2D" 中需要该属性。 |
解释:
· Point3D 的成员至少与 Point相同,则Point兼容Point3D,
2、接口之间的类型兼容性
接口之间的兼容性,类似于class。并且,class和 interface 之间也可以兼容。
1 | // 接口之间的类型兼容性 |
2 | interface Point { |
3 | x: number |
4 | y: number |
5 | } |
6 | interface Point2D { |
7 | x: number |
8 | y: number |
9 | } |
10 | interface Point3D { |
11 | x: number |
12 | y: number |
13 | z: number |
14 | } |
15 |
16 | let p1: Point = { x: 1, y: 2} |
17 | let p2: Point2D = { x: 3, y: 4} |
18 | let p3: Point3D = { x: 5, y: 6, z: 7} |
19 |
20 | p1 = p2 |
21 | p2 = p1 |
22 | p1 = p3 |
23 |
24 | // 错误演示 |
25 | p3 = p1 // 类型 "Point" 中缺少属性 "z",但类型 "Point3D" 中需要该属性 |
26 |
27 | // 类和接口之间也是兼容的 |
28 | class Point4D { |
29 | x: number |
30 | y: number |
31 | z: number |
32 | } |
33 | p2 = new Point4D() |
2、函数之间的类型兼容性
函数之间兼容性比较复杂,需要考虑:参数个数、参数类型、返回值类型。
参数个数,参数多的兼容参数少的(或者说,参数少的可以赋值给多的)。
1 | // 1 参数个数:参数少的可以赋值给参数多的 |
2 | type F1 = (a: number) => void |
3 | type F2 = (a: number, b: number) => void |
4 |
5 | let f1: F1 = () => {} |
6 | let f2: F2 = () => {} |
7 |
8 | f2 = f1 |
9 | // 错误演示 |
10 | f1 = f2 // 不能将类型“F2”分配给类型“F1”。 |
解释:
· 参数少的可以赋值给参数多的,所以,f1 可以赋值给 f2。
· 数组 forEach 方法的第一个参数是回调函数,该示例中类型为:(value: string, index: number, array: stringl) => void。
· 在 JS 中省略用不到的函数参数实际上是很常见的,这样的使用方式,促成了 TS 中函数类型之间的兼容性。
· 并且因为回调函数是有类型的,所以,TS 会自动推导出参数 item、index、array 的类型。
参数类型,相同位置的参数类型要相同(原始类型)或兼容(对象类型)。
1 | // 2 参数类型:相同位置的参数类型要相同或兼容 |
2 |
3 | // 原始类型:类型需要保持一致 |
4 | type F1 = (a: number) => void |
5 | type F2 = (a: number) => void |
6 |
7 | let f1: F1 = () => {} |
8 | let f2: F2 = () => {} |
9 |
10 | f2 = f1 |
11 | f1 = f2 |
解释:函数类型 F2兼容函数类型F1,因为 F1和 F2的第一个参数类型相同
1 | // 对象类型: |
2 | interface Point3D { |
3 | x: number |
4 | y: number |
5 | } |
6 | interface Point4D { |
7 | x: number |
8 | y: number |
9 | z: number |
10 | } |
11 |
12 | type F3 = (p: Point3D) => void // 相当于有 2 个参数 |
13 | type F4 = (p: Point4D) => void // 想当于有 3 个参数 |
14 |
15 | let f3: F3 = () => {} |
16 | let f4: F4 = () => {} |
17 |
18 | f4 = f3 |
19 | // 错误演示 不能将类型“F4”分配给类型“F3”。参数“p”和“p” 的类型不兼容。 |
20 | // f3 = f4 // 类型 "Point3D" 中缺少属性 "z",但类型 "Point4D" 中需要该属性。 |
解释:
· 注意,此处与前面讲到的接口兼容性冲突。
· 技巧:将对象拆开,把每个属性看做一个个参数,则,参数少的(f2)可以赋值给参数多的(f3)。
返回值类型,只关注返回值类型本身即可。
1 | // 3 返回值类型:只关注返回值类型本身即可 |
2 |
3 | // 原始类型: |
4 | type F5 = () => string |
5 | type F6 = () => string |
6 |
7 | let f5: F5 = () => { return '' } |
8 | let f6: F6 = () => { return '' } |
9 |
10 | f6 = f5 |
11 | f5 = f6 |
12 |
13 | // 对象类型: |
14 | type F7 = () => { name: string } |
15 | type F8 = () => { name: string; age: number } |
16 |
17 | let f7: F7 = () => { return { name: 'Jake' }} |
18 | let f8: F8 = () => { return { name: 'Jake' , age: 20 }} |
19 |
20 | f7 = f8 |
21 | // 错误演示 不能将类型“F7”分配给类型“F8”。 |
22 | //f8 = f7 // 类型 "{ name: string; }" 中缺少属性 "age",但类型 "{ name: string; age: number; }" 中需要该属性。 |
解释:
· 如果返回值类型是原始类型,此时两个类型要相同,比如,左侧类型 F5和 F6。
· 如果返回值类型是对象类型,此时成员多的可以赋值给成员少的,比如,右侧类型 F7 和 F8。
三、交叉类型(&)
交叉类型(&):功能类似于接口继承(extends),用于组合多个类型为一个类型(常用于对象类型)。
1 | // 接口 |
2 | interface Person { |
3 | name: string |
4 | } |
5 | interface Contact { |
6 | phone: string |
7 | } |
8 | // 交叉类型 |
9 | type PersonDetail = Person & Contact |
10 |
11 | let obj: PersonDetail = { |
12 | name: 'Jake' , |
13 | phone: '136……' |
14 | } |
解释:使用交叉类型后,新的类型 PersonDetail就同时具备了Person 和 Contact 的所有属性类型
交叉类型(&)和接口继承(extends)的对比:
相同点:都可以实现对象类型的组合。
不同点:两种方式实现类型组合时,对于同名属性之间,处理类型冲突的方式不同。
1 | // 对比 |
2 | interface A { |
3 | fn: (value: number) => string |
4 | } |
5 | interface B { |
6 | fn: (value: string) => string |
7 | } |
8 | interface C extends A { // 属性“fn”的类型不兼容,不能将类型“number”分配给类型“string” |
9 | fn: (value: string) => string |
10 | } |
11 |
12 | type D = A & B |
13 | // 交叉类型相当于:fn: (value:string | number) => string |
14 | let d: D = { |
15 | fn(value: number | string) { |
16 | return '' |
17 | } |
18 | } |
19 | d.fn(1) |
20 | d.fn( '2' ) |