TypeScript 的类用 extends 继承另一个类。子类获得父类的全部字段和方法,用 super(...) 调用父类构造函数,还可以重写方法,而 TypeScript 会检查每个重写都与父类的类型保持兼容。
这就是 JavaScript 的类继承(背后的原型链见 JavaScript 中的继承)。TypeScript 在此之上加了类型检查:凡是需要 Animal 的地方都可以传入 Dog,编译器还会验证重写的方法和 super 调用。
重写方法
子类中同名的方法会替换父类的方法。它的类型必须能赋值给父类的方法类型:参数相同或兼容,返回类型相同或更窄。否则,任何持有 Animal 的代码都可能用子类处理不了的参数去调用这个方法。
如果把参数改成 value: string,就会得到错误 TS2416:Property 'format' in type 'CurrencyFormatter' is not assignable to the same property in base type 'Formatter'。上面的循环说明了原因:它对每个元素都用数字调用 format,并不知道拿到的是哪个子类。
这里有一个漏洞。方法参数是双向协变比较的,所以接收更窄类型的重写,比如 format(value: 1 | 2),也能通过编译,尽管循环仍然可能传给它 1234567。返回类型则是严格检查的:返回 number 而不是 string 会报 TS2416。
override 关键字与 noImplicitOverride
override 表示“这个成员替换了基类中的某个成员”,编译器会让你说话算数。如果基类没有这样的成员(拼写错误,或者有人在父类里改了方法名),就会报错:
index.ts(9,14): error TS4117: This member cannot have an 'override' modifier because it is not declared in the base class 'Animal'. Did you mean 'speak'?
不写 override 的话,这个拼写错误能通过编译,speak() 会悄无声息地返回 "..."。这个关键字只在写了它的地方起作用,所以要在 tsconfig.json 中开启 noImplicitOverride。之后每个重写基类成员的方法都必须声明出来,子类里一个普通的 speak() 就会报错 TS4114(This member must have an 'override' modifier because it overrides a member in the base class 'Animal')。
{
"compilerOptions": {
"strict": true,
"noImplicitOverride": true
}
}
noImplicitOverride 不包含在 strict 里,需要你自己加上。override 在编译出的 JavaScript 中会被擦除。
子类中的 protected 成员
父类的 private 成员在子类中不可见。protected 成员是可见的,这正是 protected 存在的意义:它是父类提供给子类的 API。
完整规则见 访问修饰符,其中也解释了为什么 private 和 protected 在运行时都不会强制生效。
用更窄的类型重新声明字段
子类常常知道某个字段存放的东西比父类声明的更具体。在现代的类字段语义下,把它重新声明为 pet: Dog 会产生运行时效果:super() 返回后,子类字段会被初始化为 undefined,把父类构造函数存进去的值抹掉。除非子类构造函数再次赋值,否则 TypeScript 会报 TS2612(Property 'pet' will overwrite the base property in 'Home')。用 declare 可以只改类型:
declare 不生成任何代码,所以 Home 构造函数设置的值得以保留。让这个更窄的类型成立的,是那个只接受 Dog 的构造函数。
implements 与 extends
这两个关键字回答的是不同的问题,一个类可以同时使用两者。
extends | implements | |
|---|---|---|
| 作用对象 | 一个类 | 任意多个接口(或对象类型的类型别名) |
| 带来代码 | 是:字段、方法、构造函数 | 否,只检查结构 |
| 运行时存在 | 是,即原型链 | 否,会被擦除 |
instanceof 可用 | 是 | 否 |
| 典型用途 | 共享真实的行为 | 向其余代码承诺一种结构 |
一个类只能继承一个类(TS1174: Classes can only extend a single class)。要组合多个来源的行为,可以实现多个接口,并通过组合(用一个字段持有辅助对象)或 mixin 来共享代码。
Mixin
mixin 是一个接收类并返回其子类的函数。它提供了一种能通过类型检查的多继承形式:
Constructor 中的 any[] 是必需的:TypeScript 只接受构造函数只有一个 any[] 类型剩余参数的 mixin 基类。mixin 能用,但会让类型和调用栈更难读,所以优先考虑组合。
常见错误
- 在基类构造函数中调用可被重写的方法。 基类构造函数在子类字段初始化之前运行,所以读取
this.items的重写方法会拿到undefined,而this.items.push(x)这样的调用会在运行时抛出TypeError。TypeScript 发现不了这个问题。 - 忘记调用
super(...)或调用得太晚。 会报错TS2377和TS17009;父类必须先构建好对象,子类才能使用this。 - 重新声明父类字段时没有用
declare。 子类字段会把它重置为undefined。 - 继承层次过深。 每多一层,子类就和父类的内部实现多耦合一层。通常两层就足够了;超过两层时,优先使用组合或接口。
常见问题
TypeScript 中的继承是怎么工作的?
一个类用 extends 继承另一个类:class Dog extends Animal。子类获得父类的字段和方法,用 super(...) 调用父类构造函数,还可以重写方法。TypeScript 会检查重写后的方法与父类的类型保持兼容。
TypeScript 中的 override 关键字有什么用?
override 标记一个替换了基类成员的方法或属性。如果基类没有同名成员,编译器会报 TS4117,这样能发现拼写错误和父类中被改名的成员。开启 noImplicitOverride 后,每个重写都必须带上这个关键字(否则报 TS4114)。
TypeScript 中如何调用父类的方法?
在子类方法里使用 super.methodName(...)。在构造函数中,super(...) 调用父类构造函数,并且必须在任何使用 this 的代码之前执行。
TypeScript 支持多继承吗?
不支持。一个类只能继承一个类(TS1174: Classes can only extend a single class)。它可以实现任意多个接口,而 mixin(接收一个类并返回其子类的函数)可以把多个来源的行为组合起来。
extends 和 implements 有什么区别?
extends 从父类继承真实的代码。implements 只检查类是否符合接口的结构,运行时什么也不添加。一个类可以同时使用两者:class Dog extends Animal implements Pet。