interface Child extends Parent 创建一个新接口,它拥有 Parent 的每个成员,再加上自己的成员。子类型的值可以用在任何需要父类型的地方,因为它们具备父类型的全部属性。
同一个关键字在 TypeScript 中出现在四个地方,含义相关但并不相同:
| 位置 | 示例 | 含义 |
|---|---|---|
| 接口 | interface Dog extends Animal {} | 继承父接口的成员 |
| 类 | class Dog extends Animal {} | 继承字段和方法实现(运行时的 JavaScript) |
| 泛型参数 | <T extends Animal> | 约束:类型实参必须能赋值给 Animal |
| 条件类型 | T extends string ? A : B | 判断:T 能否赋值给 string? |
扩展多个接口
在 extends 后面列出多个父接口,用逗号分隔。结果拥有所有父接口的成员:
interface Identified {
id: number;
}
interface Timestamped {
createdAt: Date;
updatedAt: Date;
}
interface Post extends Identified, Timestamped {
title: string;
}
// Post requires id, createdAt, updatedAt and title
如果两个父接口声明了同名属性,而类型又不完全相同,子接口就会报错 TS2320(Interface 'Post' cannot simultaneously extend types 'A' and 'B'),除非子接口重新声明这个属性,并给它一个能同时赋值给两者的类型。
扩展类型别名
type 别名没有 extends 子句。要在它的基础上构建新类型,用交叉类型 &,它会合并两边的成员。接口也可以直接扩展对象类型别名,类型别名也可以和接口取交叉:两者可以自由混用。
在属性发生冲突之前,这两种方式的表现完全一样:extends 会在声明处报告冲突的属性,而 & 会悄悄地把它变成 never。两者的对比见 interface 与 type 页面,& 的细节见交叉类型页面。
覆盖属性类型
子接口可以重新声明父接口的属性,但新类型必须能赋值给父接口中的类型。把 string 收窄为字面量,或者从联合类型中去掉 null,都是允许的。改成不相关的类型则不行:
index.ts(13,11): error TS2430: Interface 'Label' incorrectly extends interface 'Shape'.
Types of property 'kind' are incompatible.
Type 'number' is not assignable to type 'string'.
这条规则的目的是让 Label 仍然可以用在任何需要 Shape 的地方。如果 kind 可以是数字,那么为 Shape 编写、调用了 shape.kind.toUpperCase() 的代码就会出错。
用 Omit 替换属性
如果确实需要给继承来的属性换一个不同的类型,先用 Omit 把它从父类型中移除,再声明新的属性。常见的场景是转换 API 数据,其中的 id 和日期是以字符串形式传过来的:
Omit<ApiUser, "id" | "createdAt"> 是 { name: string },所以新的 id 和 createdAt 不会和任何东西冲突。Pick、Partial 和其他工具类型也能用同样的方式扩展。
类中的 extends
class Dog extends Animal 是 JavaScript 的类继承,和接口的 extends 不同,它在运行时真实存在:Dog 获得 Animal 的构造函数、字段和方法实现,new Dog(...) instanceof Animal 的结果是 true。TypeScript 在此基础上加入了类型检查,例如 override 关键字:
一个类只能 extends 一个类,但可以 implements 任意多个接口。implements 什么也不复制:它只检查类是否声明了接口要求的内容。super、受保护成员和 override 的内容见继承。
泛型和条件类型中的 extends
在尖括号里,extends 是一个约束。<T extends { length: number }> 接受任何带有数值 length 的类型实参,并允许函数体使用 .length。在条件类型中,T extends U ? X : Y 判断的是 T 能否赋值给 U。
在这两处,extends 的意思都是“能赋值给”,和接口形式检查的是同一种关系。约束的详细介绍见泛型约束,类型层面的判断见条件类型。
常见问题
TypeScript 中如何扩展接口?
写 interface Child extends Parent { ... }。子接口拥有父接口的所有属性,再加上它自己声明的属性。要同时扩展多个接口,用逗号分隔:interface C extends A, B { ... }。
TypeScript 中如何扩展类型别名?
类型别名不能使用 extends,要改用交叉类型来组合:type Admin = User & { permissions: string[] }。接口也可以直接扩展对象类型别名:interface Admin extends User { permissions: string[] }。
扩展接口时能覆盖属性类型吗?
只能用兼容的、更窄的类型:kind: "circle" 可以替换 kind: string。换成不同的类型,比如给 string 属性改成 number,就会报错 TS2430(Interface 'X' incorrectly extends interface 'Y')。要把属性换成不相关的类型,先把它移除:interface User extends Omit<ApiUser, "id"> { id: number }。
extends 和 implements 有什么区别?
extends 是继承:接口获得父接口的成员,类获得父类的字段和方法实现。implements 只做检查:实现某个接口的类必须自己声明每一个成员,从接口那里什么也得不到。
<T extends string> 这样的泛型里 extends 是什么意思?
它是约束,不是继承。T extends string 表示类型实参必须能赋值给 string,因此在函数内部可以对 T 使用字符串方法。在条件类型中,T extends U ? X : Y 是一个判断:如果 T 能赋值给 U,结果是 X,否则是 Y。