TypeScript学习笔记(四):高级类型一

前言

TS 中的高级类型有很多:class类、类型兼容型、交叉类型、泛型和keyof、索引签名类型和索引查询类型、映射类型

一、class 类

TypeScript 全面支持 ES2015 中引入的 class 关键字,并为其添加了类型注解和其他语法(比如,可见性修饰符等)

1、class 基本使用,如下

1// 定义 class 类
2class Person {}
3 
4// 创建实例,通过 ts 的类型推论得出 const p: Person
5const p = new Person()

解释:
·  根据 TS 中的类型推论,可以知道Person类的实例对象p的类型是Person。
·  TS 中的 class,不仅提供了 class 的语法功能,也作为一种类型存在。

实例属性初始化:

1// 实例属性初始化,能省则省
2class Person {
3    age: number
4    gender = '男' // gender: string = '男'
5}
6 
7// 创建实例,通过 ts 的类型推论得出 const p: Person
8const p = new Person()
9 
10p.age // (property) age: number
11p.gender // (property) gender: string

解释:
·  声明成员 age,类型为 number(没有初始值)。
·  声明成员 gender,并设置初始值,此时,可省略类型注解(TS类型推论为string类型)。

2、构造函数

1class 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}
11const p = new Person(28, '男')
12 
13console.log(p.age, p.gender) // 28 男

解释:
·  成员初始化(比如,age:number)后,才可以通过this.age 来访问实例成员。
·  需要为构造函数指定类型注解,否则会被隐式推断为 any;构造函数不需要返回值类型

3、实例方法

1class Point {
2    x = 10
3    y = 10
4 
5    scale(n: number): void {
6        this.x *= n
7        this.y *= n
8    }
9}
10 
11const p = new Point()
12 
13p.scale(10)
14 
15console.log(p.x, p.y) // 10 20

解释:方法的类型注解(参数和返回值)与函数用法相同。

3、类的继承

类继承的两种方式:extends(继承父类)、implements(实现接口),JS 中只有extends,而implements 是 TS 提供的。

extends(继承父类)

1// 父类
2class Animal {
3    move() {
4        console.log('走两步')
5    }
6}
7 
8// 子类
9class Dog extends Animal {
10    name = '二哈'
11 
12    bark() {
13        console.log('旺旺!')
14    }
15}
16 
17const d = new Dog()
18d.move() // 走两步
19d.bark() // 旺旺!
20console.log(d.name) // 二哈

解释:
·  通过 extends 关键字实现继承
·  子类 Dog 继承父类 Animal,则 Dog 的实例对象 dog 就同时具有了父类Animal和 子类 Dog 的所有属性和方法。

implements(实现接口)

1interface Singale {
2    name: string
3    sing(): void
4}
5// 如果缺少 name 属性或 sing 方法,就会报错
6class 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// 父类
2class Animal {
3    public move() {
4        console.log('走两步')
5    }
6}
7 
8const a = new Animal()
9a.move() // 走两步
10 
11// 子类
12class Dog extends Animal {
13    bark() {
14        console.log('旺旺!')
15    }
16}
17 
18const d = new Dog()
19d.move() // 走两步

解释:
·  在类属性或方法前面添加 public 关键字,来修饰该属性或方法是共有的。
·  因为 public 是默认可见性,所以,可以直接省略

protected:表示受保护的,仅对其声明所在类和子类中(非实例对象)可见

1// 父类
2class Animal {
3    // 受保护的,只能在当前类或子类的其它方法中调用
4    protected move() {
5        console.log('走两步')
6    }
7    run() {
8        this.move() // 可以调用
9    }
10}
11 
12const a = new Animal()
13a.move() // 不能调用 move 方法
14 
15// 子类
16class Dog extends Animal {
17    bark() {
18        this.move() // 可以调用
19        console.log('旺旺!')
20    }
21 
22}
23 
24const d = new Dog()
25d.move() // 不能调用 move 方法

解释:
·  在类属性或方法前面添加 protected 关键字,来修饰该属性或方法是受保护的。
·  在子类的方法内部可以通过 this 来访问父类中受保护的成员,但是,对实例不可见!

private:表示私有的,只在当前类中可见,对实例对象以及子类也是不可见的。

1// 父类
2class Animal {
3    // 当前类私有的,只能在当前类方法中调用
4    private move() {
5        console.log('走两步')
6    }
7    run() {
8        this.move() // 可以调用
9    }
10}
11 
12const a = new Animal()
13a.move() // 不能调用 move 方法
14 
15// 子类
16class Dog extends Animal {
17    bark() {
18        this.move() // 不能调用 move 方法
19        console.log('旺旺!')
20    }
21 
22}
23 
24const d = new Dog()
25d.move() // 不能调用 move 方法

解释:
·  在类属性或方法前面添加 private 关键字,来修饰该属性或方法是私有的。
·  私有的属性或方法只在当前类中可见,对子类和实例对象也都是不可见的!

readonly(只读修饰符)除了可见性修饰符之外的常见修饰符。
readonly:表示只读,用来防止在构造函数之外对属性进行赋值

1class 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 (字面量类型)。

1class 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// 不使用接口指定类型
11let obj: {readonly name: string} = {
12    name: 'jack'
13}
14 
15obj.name = 'rose' // 无法为“name”赋值,因为它是只读属性。

二、类型兼容性

两种类型系统:StructuralTypeSystem(结构化类型系统)、NominalTypeSystem(标明类型系统)
TS 采用的是结构化类型系统,也叫做 ducktyping(鸭子类型),类型检查关注的是值所具有的形状
也就是说,在结构类型系统中,如果两个对象具有相同的形状,则认为它们属于同一类型

1 

解释:
·  Point和 Point2D 是两个名称不同的类。
·  变量 p 的类型被显示标注为 Point 类型,但是,它的值却是 Point2D 的实例,并且没有类型错误。
·  因为 TS 是结构化类型系统,只检查 Point 和 Point2D 的结构是否相同(相同,都具有x和y两个属性,属性类型也相同)了。
·  但是,如果在 NominalType System 中(比如,C#、Java等),它们是不同的类,类型无法兼容。

1// 演示类型兼容性
2let arr = ['a', 'b', 'c']
3 
4// forEach的回调函数,因为类型兼容型的存在,在 TS 中下面写法都是可以的
5arr.forEach(item => {})
6arr.forEach((item, index) => {})
7arr.forEach((item, index, array) => {})
8 
9// 两个类的兼容性演示
10class Point {
11    x: number
12    y: number
13}
14class Point2D {
15    x: number
16    y: number
17}
18// 因为 TS 是结构化类型系统,只检查 Point 和 Point2D 的结构是否相同(相同,都具有x和y两个属性,属性类型也相同)了。
19const p: Point = new Point2D()

1、对象之间的类型兼容性

注意:在结构化类型系统中,如果两个对象具有相同的形状,则认为它们属于同一类型,这种说法并不准确。
更准确的说法:对于对象类型来说,v的成员至少与x相同,则x兼容v(成员多的可以赋值给少的)

1// 对象之间的类型兼容性
2class Point {
3    x: number
4    y: number
5}
6 
7class Point2D {
8    x: number
9    y: number
10    z: number
11}
12 
13const p1: Point = new Point2D()
14// 错误演示
15// const p2: Point2D = new Point() // 类型 "Point" 中缺少属性 "z",但类型 "Point2D" 中需要该属性。

解释:
·  Point3D 的成员至少与 Point相同,则Point兼容Point3D,

2、接口之间的类型兼容性

接口之间的兼容性,类似于class。并且,class和 interface 之间也可以兼容。

1// 接口之间的类型兼容性
2interface Point {
3    x: number
4    y: number
5}
6interface Point2D {
7    x: number
8    y: number
9}
10interface Point3D {
11    x: number
12    y: number
13    z: number
14}
15 
16let p1: Point = { x: 1, y: 2}
17let p2: Point2D = { x: 3, y: 4}
18let p3: Point3D = { x: 5, y: 6, z: 7}
19 
20p1 = p2
21p2 = p1
22p1 = p3
23 
24// 错误演示
25p3 = p1 // 类型 "Point" 中缺少属性 "z",但类型 "Point3D" 中需要该属性
26 
27// 类和接口之间也是兼容的
28class Point4D {
29    x: number
30    y: number
31    z: number
32}
33p2 = new Point4D()

2、函数之间的类型兼容性

函数之间兼容性比较复杂,需要考虑:参数个数、参数类型、返回值类型。

参数个数,参数多的兼容参数少的(或者说,参数少的可以赋值给多的)

1// 1 参数个数:参数少的可以赋值给参数多的
2type F1 = (a: number) => void
3type F2 = (a: number, b: number) => void
4 
5let f1: F1 = () => {}
6let f2: F2 = () => {}
7 
8f2 = f1
9// 错误演示
10f1 = f2 // 不能将类型“F2”分配给类型“F1”。

解释:
·  参数少的可以赋值给参数多的,所以,f1 可以赋值给 f2。
·  数组 forEach 方法的第一个参数是回调函数,该示例中类型为:(value: string, index: number, array: stringl) => void。
·  在 JS 中省略用不到的函数参数实际上是很常见的,这样的使用方式,促成了 TS 中函数类型之间的兼容性
·  并且因为回调函数是有类型的,所以,TS 会自动推导出参数 item、index、array 的类型。

参数类型,相同位置的参数类型要相同(原始类型)或兼容(对象类型)

1// 2 参数类型:相同位置的参数类型要相同或兼容
2 
3// 原始类型:类型需要保持一致
4type F1 = (a: number) => void
5type F2 = (a: number) => void
6 
7let f1: F1 = () => {}
8let f2: F2 = () => {}
9 
10f2 = f1
11f1 = f2

解释:函数类型 F2兼容函数类型F1,因为 F1和 F2的第一个参数类型相同

1// 对象类型:
2interface Point3D {
3    x: number
4    y: number
5}
6interface Point4D {
7    x: number
8    y: number
9    z: number
10}
11 
12type F3 = (p: Point3D) => void // 相当于有 2 个参数
13type F4 = (p: Point4D) => void // 想当于有 3 个参数
14 
15let f3: F3 = () => {}
16let f4: F4 = () => {}
17 
18f4 = f3
19// 错误演示 不能将类型“F4”分配给类型“F3”。参数“p”和“p” 的类型不兼容。
20// f3 = f4 // 类型 "Point3D" 中缺少属性 "z",但类型 "Point4D" 中需要该属性。

解释:
·  注意,此处与前面讲到的接口兼容性冲突。
·  技巧:将对象拆开,把每个属性看做一个个参数,则,参数少的(f2)可以赋值给参数多的(f3)。

返回值类型,只关注返回值类型本身即可

1// 3 返回值类型:只关注返回值类型本身即可
2 
3// 原始类型:
4type F5 = () => string
5type F6 = () => string
6 
7let f5: F5 = () => { return '' }
8let f6: F6 = () => { return '' }
9 
10f6 = f5
11f5 = f6
12 
13// 对象类型:
14type F7 = () => { name: string }
15type F8 = () => { name: string; age: number }
16 
17let f7: F7 = () => { return { name: 'Jake' }}
18let f8: F8 = () => { return { name: 'Jake', age: 20 }}
19 
20f7 = f8
21// 错误演示 不能将类型“F7”分配给类型“F8”。
22//f8 = f7 // 类型 "{ name: string; }" 中缺少属性 "age",但类型 "{ name: string; age: number; }" 中需要该属性。

解释:
·  如果返回值类型是原始类型,此时两个类型要相同,比如,左侧类型 F5和 F6。
·  如果返回值类型是对象类型,此时成员多的可以赋值给成员少的,比如,右侧类型 F7 和 F8。

三、交叉类型(&)

交叉类型(&):功能类似于接口继承(extends),用于组合多个类型为一个类型(常用于对象类型)。

1// 接口
2interface Person {
3    name: string
4}
5interface Contact {
6    phone: string
7}
8// 交叉类型
9type PersonDetail = Person & Contact
10 
11let obj: PersonDetail = {
12    name: 'Jake',
13    phone: '136……'
14}

解释:使用交叉类型后,新的类型 PersonDetail就同时具备了Person 和 Contact 的所有属性类型

交叉类型(&)和接口继承(extends)的对比:
相同点:都可以实现对象类型的组合。
不同点:两种方式实现类型组合时,对于同名属性之间,处理类型冲突的方式不同

1// 对比
2interface A {
3    fn: (value: number) => string
4}
5interface B {
6    fn: (value: string) => string
7}
8interface C extends A { // 属性“fn”的类型不兼容,不能将类型“number”分配给类型“string”
9    fn: (value: string) => string
10}
11 
12type D = A & B
13// 交叉类型相当于:fn: (value:string | number) => string
14let d: D = {
15    fn(value: number | string) {
16        return ''
17    }
18}
19d.fn(1)
20d.fn('2')
赞(0) 打赏

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续提供更多优质内容,让我们一起创建更加美好的网络世界!

支付宝扫一扫

微信扫一扫