TypeScript 中的 extends 是一个非常重要的关键字,主要有 4 种用法:
- 接口继承:
interface B extends A - 类继承:
class B extends A - 泛型约束:
T extends Type - 条件类型:
T extends U ? X : Y
其中,前两种比较容易理解,泛型约束和条件类型是 TypeScript 中最值得掌握的部分。
下面通过例子逐一说明。
1. interface extends:接口继承
假设我们定义一个用户接口:
typescript
interface User {
id: number;
name: string;
}
现在需要定义管理员类型,管理员除了用户属性,还有额外的权限属性。
php
interface Admin extends User {
permissions: string[];
}
此时 Admin 相当于:
ini
interface Admin {
id: number;
name: string;
permissions: string[];
}
使用:
css
const admin: Admin = {
id: 1001,
name: "张三",
permissions: ["read", "write"]
};
核心理解:Admin extends User 表示 Admin 继承 User 的属性约束。
多接口继承
TypeScript 允许一个接口继承多个接口。
typescript
interface User {
id: number;
name: string;
}
interface Permission {
canEdit: boolean;
canDelete: boolean;
}
interface Admin extends User, Permission {
role: string;
}
使用:
yaml
const admin: Admin = {
id: 1,
name: "张三",
canEdit: true,
canDelete: false,
role: "admin"
};
2. class extends:类继承
class extends 与 Java、JavaScript 中的类继承非常相似。
typescript
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
eat() {
console.log(`${this.name} is eating`);
}
}
class Dog extends Animal {
bark() {
console.log(`${this.name} is barking`);
}
}
const dog = new Dog("旺财");
dog.eat(); // 旺财 is eating
dog.bark(); // 旺财 is barking
这里:
scala
class Dog extends Animal
表示 Dog 继承 Animal。
Dog 可以使用 Animal 的公共属性和方法,同时还可以定义自己的方法。
super 的使用
如果子类需要自定义构造函数,需要调用 super()。
typescript
class Animal {
constructor(public name: string) {}
}
class Dog extends Animal {
constructor(
name: string,
public age: number
) {
super(name);
}
}
const dog = new Dog("旺财", 3);
console.log(dog.name); // 旺财
console.log(dog.age); // 3
super(name) 用于调用父类构造函数。
3. 泛型 extends:约束泛型类型(重点)
这是 TypeScript 中非常常用的写法。
先看普通泛型:
scss
function printValue<T>(value: T): T {
return value;
}
printValue(123);
printValue("hello");
printValue({ name: "张三" });
这里的 T 可以是任意类型。
但如果希望 T 必须具有某些属性,就需要 extends。
示例一:要求对象必须包含 id
typescript
interface HasId {
id: number;
}
function printId<T extends HasId>(value: T): void {
console.log(value.id);
}
调用:
php
printId({ id: 1, name: "张三" }); // ✅
printId({ id: 2, age: 20 }); // ✅
printId({ name: "李四" }); // ❌
解释:
java
T extends HasId
表示:
泛型 T 必须满足 HasId 类型的结构要求,至少包含 id: number。
注意,它并不要求 T 与 HasId 完全相同。
示例二:限制泛型必须具有 length 属性
r
function getLength<T extends { length: number }>(value: T): number {
return value.length;
}
使用:
scss
getLength("hello"); // ✅ 5
getLength([1, 2, 3]); // ✅ 3
getLength({ length: 10 }); // ✅ 10
getLength(123); // ❌ number 没有 length 属性
这个例子非常典型。
因为:
r
T extends { length: number }
要求 T 必须有一个 length 属性,而且该属性是 number。
示例三:结合 React renderItem
结合你前面问过的 renderItem,可以这样使用:
typescript
import type { ReactNode } from "react";
interface BaseItem {
id: number;
}
interface ListProps<T extends BaseItem> {
data: T[];
renderItem: (item: T) => ReactNode;
}
function MyList<T extends BaseItem>({
data,
renderItem
}: ListProps<T>) {
return (
<div>
{data.map((item) => (
<div key={item.id}>
{renderItem(item)}
</div>
))}
</div>
);
}
使用:
typescript
interface User {
id: number;
name: string;
}
const users: User[] = [
{ id: 1, name: "张三" },
{ id: 2, name: "李四" }
];
<MyList
data={users}
renderItem={(item) => (
<span>{item.name}</span>
)}
/>
这里:
java
T extends BaseItem
保证了 item.id 一定存在。
同时又保留了其他属性,比如 name。
这就是泛型 extends 最典型的应用场景之一。
4. 条件类型 extends:类型判断(重点)
这个用法与前面几个不太一样。
语法:
java
T extends U ? X : Y
可以理解为:
如果 T 可以赋值给 U,则返回类型 X,否则返回类型 Y。
类似于 JavaScript 中的三元运算符:
sql
condition ? value1 : value2
但条件类型发生在 TypeScript 的类型系统中,不是运行时判断。
示例一:判断是否为 string
typescript
type IsString<T> = T extends string ? true : false;
使用:
ini
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<"hello">; // true
这里得到的 true、false 是布尔字面量类型。
示例二:根据类型返回不同结果
typescript
type Result<T> = T extends string ? string[] : number[];
使用:
ini
type A = Result<string>;
// string[]
type B = Result<number>;
// number[]
这个语法非常适合构建复杂的工具类型。
示例三:联合类型分发
这是条件类型中特别重要的一点。
typescript
type ToArray<T> = T extends unknown ? T[] : never;
使用:
ini
type Result = ToArray<string | number>;
结果:
ini
type Result = string[] | number[];
原因是 T 是裸类型参数,条件类型会对联合类型逐个判断。
相当于:
swift
ToArray<string> | ToArray<number>
如果不希望分发,可以这样写:
css
type ToArray<T> =
[T] extends [unknown] ? T[] : never;
此时:
ini
type Result = ToArray<string | number>;
// (string | number)[]
5. extends 和 keyof 结合使用
这在实际开发中非常常见。
假设有一个对象:
typescript
interface User {
id: number;
name: string;
age: number;
}
希望编写一个函数,安全地获取对象属性。
r
function getProperty<T, K extends keyof T>(
obj: T,
key: K
): T[K] {
return obj[key];
}
使用:
ini
const user = {
id: 1,
name: "张三",
age: 25
};
const name = getProperty(user, "name");
// 推断为 string
const age = getProperty(user, "age");
// 推断为 number
getProperty(user, "address"); // ❌
我们拆开理解:
java
K extends keyof T
T:对象类型。keyof T:获取对象所有属性名组成的联合类型。K extends keyof T:限制 K 必须是 T 的合法属性名。T[K]:获取对应属性的值类型。
例如:
bash
type UserKeys = keyof User;
// "id" | "name" | "age"
所以:
java
K extends keyof User
就相当于:
javascript
K extends "id" | "name" | "age"
这是一种非常常见的类型安全编程方式。
6. extends 和 infer 结合使用(进阶)
infer 可以在条件类型中推断出某个类型。
例如获取数组元素类型:
r
type ElementType<T> =
T extends (infer U)[] ? U : T;
使用:
ini
type A = ElementType<string[]>;
// string
type B = ElementType<number[]>;
// number
type C = ElementType<boolean>;
// boolean
解释:
scss
T extends (infer U)[]
可以理解为:
如果 T 是数组,就把数组元素类型提取出来,并命名为 U。
再举个例子,提取函数的返回值类型:
typescript
type MyReturnType<T> =
T extends (...args: any[]) => infer R
? R
: never;
使用:
bash
function getUser() {
return {
id: 1,
name: "张三"
};
}
type UserType = MyReturnType<typeof getUser>;
结果:
ini
type UserType = {
id: number;
name: string;
};
实际上,TypeScript 已经内置了 ReturnType<T>,原理与此类似。
7. 四种 extends 用法总结
| 用法 | 示例 | 作用 |
|---|---|---|
| 接口继承 | interface B extends A |
继承接口结构 |
| 类继承 | class B extends A |
继承父类 |
| 泛型约束 | T extends User |
限制泛型允许的类型 |
| 条件类型 | T extends U ? X : Y |
根据类型兼容关系选择类型 |
最容易混淆的两个用法
泛型约束
r
function test<T extends string>(value: T) {
return value;
}
含义:T 必须是 string 的子类型(可赋值给 string)。
条件类型
typescript
type Test<T> = T extends string
? number
: boolean;
含义:如果 T 满足 string 类型约束,选择 number,否则选择 boolean。
建议学习顺序:
先掌握 interface extends,然后重点研究 T extends Type、K extends keyof T,最后再学习 T extends U ? X : Y 和 infer。
因为在实际 React + TypeScript 项目中,泛型约束和 keyof 的组合会特别频繁地出现,尤其是列表组件、表格组件、表单组件和公共工具函数。