类(class)是面向对象编程的基本构件,封装了属性和方法,TypeScript 给予了全面支持

class 类型 - 图1

属性的类型

class 类型 - 图2

readonly 修饰符

class 类型 - 图3

方法的类型

class 类型 - 图4

class 类型 - 图5

存取器方法

存取器(accessor)是特殊的类方法,包括取值器(getter)存值器(setter)两种方法。它们用于读写某个属性,取值器用来读取属性,存值器用来写入属性

class 类型 - 图6

class 类型 - 图7

属性索引

注意,由于类的方法是一种特殊属性属性值为函数的属性),所以属性索引的类型定义也涵盖了方法。如果一个对象同时定义了属性索引和方法,那么前者必须包含后者的类型

class 类型 - 图8

上面示例中,属性索引的类型里面不包括方法,导致后面的方法 f() 定义直接报错。正确的写法是下面这样

class 类型 - 图9

属性存取器视同属性

class 类型 - 图10

上面示例中,属性 inInstance 的读取器虽然是一个函数方法,但是视同属性,所以属性索引虽然没有涉及方法类型,但是不会报错

类的 interface 接口

类使用 implements 关键字,表示当前类满足这些外部类型条件的限制

class 类型 - 图11

interface 只是指定检查条件,如果不满足这些条件就会报错。它并不能代替 class 自身的类型声明

class 类型 - 图12

上面示例中,类B实现了接口A,但是后者并不能代替B的类型声明。因此,B的get()方法的参数s的类型是any,而不是string。B类依然需要声明参数s的类型

class 类型 - 图13

类可以定义接口没有声明的方法和属性。表示除了满足接口给出的条件,类还有额外的条件

class 类型 - 图14

implements 关键字后面,不仅可以是接口,也可以是另一个类。这时,后面的类将被当作接口。示例中,implements 后面是类 Car,这时 TypeScript 就把 Car 视为一个接口,要求 MyCar 实现 Car 里面的每一个属性和方法,否则就会报错。所以,这时不能因为 Car 类已经实现过一次,而在 MyCar 类省略属性或方法

class 类型 - 图15

注意,interface 描述的是类的对外接口,也就是实例的公开属性和公开方法,不能定义私有的属性和方法。这是因为 TypeScript 设计者认为,私有属性是类的内部实现,接口作为模板,不应该涉及类的内部代码写法

class 类型 - 图16

实现多个接口

class 类型 - 图17

但是,同时实现多个接口并不是一个好的写法,容易使得代码难以管理,可以使用两种方法替代

  1. 第一种方法是类的继承

class 类型 - 图18

  1. 第二种方法是接口的继承

class 类型 - 图19

类与接口的合并

class 类型 - 图20

实例类型

TypeScript 的类本身就是一种类型但是它代表该类的实例类型,而不是 class 的自身类型

class 类型 - 图21

对于引用实例对象的变量来说,既可以声明类型为 Class,也可以声明类型为 Interface,因为两者都代表实例对象的类型。示例中,变量的类型可以写成类Car,也可以写成接口MotorVehicle。它们的区别是,如果类Car有接口MotoVehicle没有的属性和方法,那么只有变量c1可以调用这些属性和方法

class 类型 - 图22

作为类型使用时,类名只能表示实例的类型,不能表示类的自身类型

class 类型 - 图23

由于类名作为类型使用,实际上代表一个对象,因此可以把类看作为对象类型起名。事实上,TypeScript 有三种方法可以为对象类型起名typeinterface class

类的自身类型

要获得一个类的自身类型,一个简便的方法就是使用 typeof 运算符

class 类型 - 图24

JavaScript 语言中,类只是构造函数的一种语法糖,本质上是构造函数的另一种写法。所以,类的自身类型可以写成构造函数的形式

class 类型 - 图25

构造函数也可以写成对象形式

class 类型 - 图26

根据上面的写法,可以把构造函数提取出来,单独定义一个接口(interface),这样可以大大提高代码的通用性

class 类型 - 图27

总结一下,类的自身类型就是一个构造函数,可以单独定义一个接口来表示

结构类型原则

Class 也遵循“结构类型原则”。一个对象只要满足 Class 的实例结构,就跟该 Class 属于同一个类型

class 类型 - 图28

class 类型 - 图29

注意,确定两个类的兼容关系时,只检查**实例成员不考虑**静态成员构造方法

class 类型 - 图30

如果类中存在私有成员(private)或保护成员(protected),那么确定兼容关系时,TypeScript 要求私有成员和保护成员来自同一个类这意味着两个类需要存在继承关系

class 类型 - 图31

类的继承

类(这里又称“子类”)可以使用 extends 关键字继承另一个类(这里又称“基类”)的所有属性和方法

class 类型 - 图32

如果基类包括保护成员(protected修饰符),子类可以将该成员的可访问性设置为公开(public修饰符),也可以保持保护成员不变,但是不能改用私有成员(private修饰符)

class 类型 - 图33

注意extends 关键字后面不一定是类名,可以是一个表达式,只要它的类型是构造函数就可以了(因为类的本质也是构造函数)

class 类型 - 图34

对于那些只设置了类型、没有初值的顶层属性,有一个细节需要注意。下面示例中,类DogHouse的顶层成员resident只设置了类型(Dog),没有设置初值。这段代码在不同的编译设置下,编译结果不一样

class 类型 - 图35

如果编译设置的 target 设成大于等于 ES2022,或者 useDefineForClassFields 设成 true,那么下面代码的执行结果是不一样的

class 类型 - 图36

上面示例中,DogHouse实例的属性resident输出的是undefined,而不是预料的dog。原因在于 ES2022 标准的 Class Fields 部分,与早期的 TypeScript 实现不一致,导致子类的那些只设置类型、没有设置初值的顶层成员在基类中被赋值后,会在子类被重置为 undefined

详细的解释参见《tsconfig.json》一章,以及官方 3.7 版本的发布说明

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#the-usedefineforclassfields-flag-and-the-declare-property-modifier

解决方法就是使用 declare 命令,去声明顶层成员的类型,告诉 TypeScript 这些成员的赋值由基类实现

class 类型 - 图37

上面示例中,resident属性的类型声明前面用了declare命令,这样就能确保在编译目标大于等于 ES2022 时(或者打开 useDefineForClassFields 时),代码行为正确

可访问性修饰符

  • public 修饰符表示这是公开成员,外部可以自由访问
  • private 修饰符表示私有成员,只能用在当前类的内部类的实例子类都不能使用该成员
  • protected 修饰符表示该成员是保护成员,只能在类的内部使用该成员,实例无法使用

严格地说,private 定义的私有成员,并不是真正意义的私有成员

  1. 编译成 JavaScript 后,private关键字就被剥离了,这时外部访问该成员就不会报错
  2. 由于前一个原因,TypeScript 对于访问 private 成员没有严格禁止,使用方括号写法([])或者 in 运算符,实例对象就能访问该成员

class 类型 - 图38

由于 private 存在这些问题,加上它是 ES2022 标准发布前出台的,而 ES2022 引入了自己的私有成员写法 **#propName因此建议不使用private,改用 ES2022 的写法,获得真正意义的私有成员**

class 类型 - 图39

构造方法也可以是私有的,这就直接防止了使用 new 命令生成实例对象,只能在类的内部创建实例对象这时一般会有一个静态方法,充当工厂函数,强制所有实例都通过该方法生成

class 类型 - 图40

实例属性的简写形式

class 类型 - 图41

class 类型 - 图42

静态成员

类的内部可以使用 static 关键字,定义静态成员。静态成员是只能通过类本身使用的成员,不能通过实例对象使用。static 关键字前面可以使用 public、.private、.protected 修饰符

class 类型 - 图43

静态私有属性也可以用 ES6 语法的 #前缀 表示,上面示例可以改写如下

class 类型 - 图44

public 和 protected 的静态成员可以被继承

class 类型 - 图45

泛型类

class 类型 - 图46

注意静态成员不能使用泛型的类型参数

class 类型 - 图47

上面示例中,静态属性 defaultContents 的类型写成类型参数 Type 会报错。因为这意味着调用时必须给出类型参数(即写成Box.defaultContents),并且类型参数发生变化,这个属性也会跟着变,这并不是好的做法

抽象类|抽象成员

TypeScript 允许在类的定义前面,加上关键字 abstract,表示该类不能被实例化,只能当作其他类的模板。这种类就叫做“抽象类”(abstract class)

class 类型 - 图48

抽象类的内部可以有已经实现好的属性和方法,也可以有还未实现的属性和方法。后者就叫做“抽象成员”(abstract member),即属性名和方法名有 abstract 关键字,表示该方法需要子类实现。如果子类没有实现抽象成员,就会报错

class 类型 - 图49

this 问题

class 类型 - 图50

有些场合需要给出this类型,但是 JavaScript 函数通常不带有this参数,这时 TypeScript 允许函数增加一个名为this的参数,放在参数列表的第一位,用来描述函数内部的this关键字的类型。下面示例中,函数fn()的第一个参数是this,用来声明函数内部的this的类型。编译时,TypeScript 一旦发现函数的第一个参数名为this,则会去除这个参数,即编译结果不会带有该参数

class 类型 - 图51

下面示例中,类 A 的 getName() 添加了 this 参数,如果直接调用这个方法,this 的类型就会跟声明的类型不一致,从而报错

class 类型 - 图52

this 参数的类型可以声明为各种对象。下面示例中,参数 this 的类型是一个带有 name 属性的对象,不符合这个条件的 this 都会报错

class 类型 - 图53

TypeScript 提供了一个 noImplicitThis 编译选项。如果打开了这个设置项,如果 this 的值推断为 any 类型,就会报错。示例中, getAreaFunction() 方法返回一个函数,这个函数里面用到了 this,但是这个 this 跟 Rectangle 这个类没关系,它的类型推断为 any,所以就报错了

class 类型 - 图54

在类的内部,this 本身也可以当作类型使用,表示当前类的实例对象

class 类型 - 图55

有些方法返回一个布尔值,表示当前的 this 是否属于某种类型。这时,这些方法的返回值类型可以写成 this is Type 的形式,其中用到了 is 运算符

class 类型 - 图56

上面示例中,两个方法的返回值类型都是布尔值,写成 this is Type 的形式,可以精确表示返回值