Menu

TypeScript 类(class):字段、方法、getter 与 implements

TypeScript 类就是带有类型的字段、方法和构造函数的 JavaScript 类。本文讲解字段声明和 strictPropertyInitialization 如何工作、如何为 this 标注类型、getter 和 setter、静态成员、implements,以及类为什么同时也是一个类型。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

TypeScript 类就是带类型的 JavaScript 类:每个字段在类体中声明类型,方法和构造函数的参数与返回值也都带有类型。

编译输出是一个去掉了类型的普通 JavaScript 类,所以 JavaScript 类中的所有知识依然适用。TypeScript 增加的检查发生在编译期:最后一次调用在程序运行之前就被拒绝了。

声明字段

字段必须先在类体中声明,构造函数才能给它赋值。没有声明 owner 就给 this.owner 赋值,会报 TS2339 错误(属性不存在)。带初始值的字段会像变量一样,从值推断出类型。

声明含义
name: string;必须在构造函数中赋值
count = 0;已初始化,类型推断为 number
label?: string;可选,类型为 string | undefined
readonly id: number;只赋值一次,之后只读
data!: string[];明确赋值:相信我,它会被赋值
static total = 0;属于类本身,而不属于实例

开启 strict 后,strictPropertyInitialization 会检查每个非可选字段是否都得到了值。下面这个类漏掉了两个:

编译器把两个都报了出来:

index.ts(3,5): error TS2564: Property 'name' has no initializer and is not definitely assigned in the constructor.
index.ts(4,5): error TS2564: Property 'age' has no initializer and is not definitely assigned in the constructor.

之所以有这项检查,是因为如果没有它,new Profile().name.toUpperCase() 会通过类型检查,然后因为 undefined 而崩溃。每个字段都可以用四种方法之一修复:给它初始值、在构造函数中赋值、用 ? 标记为可选,或者在框架或 init() 方法稍后会赋值时写成 name!: string。最后一种会关闭该字段的检查,所以要谨慎使用。

方法与 this

方法的类型标注方式和函数一样。在方法内部,this 的类型就是实例的类型。方法可以返回 this 来支持链式调用,而且返回类型会自动跟随子类。

class QueryBuilder {
    private parts: string[] = [];

    where(condition: string): this {
        this.parts.push(condition);
        return this;
    }

    build(): string {
        return this.parts.join(" AND ");
    }
}

const sql = new QueryBuilder().where("age > 18").where("active = 1").build();
console.log(sql); // age > 18 AND active = 1

把方法从对象上单独取出来时,它会丢失 this,这是 JavaScript 的经典 bug。TypeScript 默认不会发现这个问题,但如果方法声明了 this 参数,它就能发现。这个参数会从输出中擦除,只用于守护调用处:

箭头函数字段之所以可行,是因为每个实例都有自己的一个函数,闭包捕获了 this。代价是每个实例一个函数,而不是在原型上共享一个方法。

getter 和 setter

get 和 set 访问器从外部看就像属性。只有 getter 没有 setter 的属性是只读的:给它赋值会产生编译错误(TS2540)。

编译器拒绝了这次赋值;即使绕过了检查,运行时也会抛出 TypeError,因为编译出的文件运行在严格模式下(编译器会在文件顶部生成 "use strict")。setter 中的 RangeError 是你自己写的运行时检查。类型 number 只说明值是数字,它对取值范围一无所知。

静态成员

static 字段和方法属于类本身。你要通过类名而不是实例来访问它们。static { } 块会在类定义时运行一次。

静态成员不能使用类的类型参数:在 class Box<T> 中,static empty: T 会报 TS2302 错误,因为静态字段只有一个,由所有 Box<string>、Box<number> 等共享。

实现接口

implements 让编译器检查这个类是否具备接口要求的一切。缺少成员会报 TS2420 错误:Class 'X' incorrectly implements interface 'Y'。

一个类可以实现多个接口:class Doc implements Printable, Serializable。implements 有两件事不会做。第一,它不会替你给类方法的参数标注类型:在实现了 check(s: string): boolean 的类中写 check(s) { ... },依然会报 TS7006 错误(Parameter 's' implicitly has an 'any' type),所以要自己加上注解。第二,它在运行时不留任何痕迹,所以无法写 obj instanceof Shape。

构造函数中的 private radius 是参数属性:它一步完成字段的声明和赋值。参见构造函数。

类同时也是类型

一个类声明会用同一个名字创建两样东西:一个值(用 new 调用的构造函数)和一个类型(实例的形状)。因为 TypeScript 按结构比较类型,任何拥有相同公共成员的对象都符合这个类型,即使它不是由这个类创建的。

这个普通对象通过了类型检查,但在运行时并不是 Point:({ x: 6, y: 8 }) instanceof Point 的结果是 false。如果类有 private 或 #private 成员,就能阻止这种情况:只有该类(或其子类)的实例才能赋值给它。

常见错误

  • 声明了字段却从不赋值。 TS2564 是在告诉你这个字段会是 undefined。应该初始化它,而不是用 ! 让它闭嘴。
  • 把方法当作回调传递。 button.onclick = obj.handle 会丢失 this。使用箭头函数字段或 obj.handle.bind(obj)。
  • 以为 implements 会添加代码或类型。 它只负责检查。参数类型仍然需要注解。
  • 对接口使用 instanceof。 接口在运行时不存在;请检查类,或者使用类型守卫。
  • 以为类型会校验数据。 number 字段在编译期接受任何数字,而在运行时,对于没有类型的数据,它什么都接受。

常见问题

如何在 TypeScript 中创建类?

在类体中为每个字段声明类型,然后像 JavaScript 那样编写构造函数和方法:class User { name: string; constructor(name: string) { this.name = name; } greet(): string { return "Hi, " + this.name; } }。用 new User("Ada") 创建实例。

“has no initializer and is not definitely assigned in the constructor” 是什么意思?

这是 strictPropertyInitialization(属于 strict 的一部分)报出的 TS2564 错误。类型为 string 的字段如果没有任何地方给它赋值,一开始就会是 undefined。修复方法有:添加初始值(name = "")、在构造函数中赋值、改为可选字段(name?: string),或者在构造函数之外的代码负责赋值时,使用明确赋值断言(name!: string)。

TypeScript 中 implements 和 extends 有什么区别?

extends 从父类继承代码:父类的字段和方法在运行时存在于子类上。implements 只是让编译器检查这个类是否具有某个接口的形状。它不会给类添加任何东西,在编译出的 JavaScript 中也会消失。

在 TypeScript 中类可以当作类型使用吗?

可以。类名既是一个值(构造函数),也是一个类型(实例的形状)。let u: User 接受任何拥有相同公共成员的对象,因为 TypeScript 按结构比较类型。typeof User 则是构造函数本身的类型。

TypeScript 类在运行时和 JavaScript 类有区别吗?

没有。TypeScript 把类编译成普通的 JavaScript 类。类型注解、implements 子句以及 private 这类修饰符都会被擦除;只有 #private 字段、static 块和 getter 这类 JavaScript 特性在运行时存在。

Coddy 编程语言插图

和 Coddy 一起学编程

开始