TypeScript 中的泛型(Generics) 是类型系统中非常强大且核心的功能。简单来说,泛型允许你编写 “可复用” 的代码组件(如函数、类、接口),这些组件可以处理多种数据类型,同时还能保持类型安全。
你可以把泛型理解为: “类型的参数化” 。就像函数接收参数一样,泛型让类型本身也能接收参数。
1. 为什么要用泛型?(解决什么问题)
如果没有泛型,当你想要编写一个“原样返回输入值”的函数时,会面临两难选择:
❌ 方案 A:使用 any(丢失类型检查)
function identity(arg: any): any {
return arg;
}
const result = identity("Hello");
// result 的类型是 any,编辑器无法提示字符串方法,失去了 TS 的意义 result.toFixed(); // ❌ 运行时才会报错,TS 编译时不报错
❌ 方案 B:写多个重载函数(代码冗余)
function identityString(arg: string): string { return arg; }
function identityNumber(arg: number): number { return arg; }
// 如果有 100 种类型,就要写 100 个函数...
✅ 方案 C:使用泛型(完美方案)
// <T> 声明了一个类型变量 T
function identity<T>(arg: T): T {
return arg;
}
// 调用时明确指定 T 为 string
const str = identity<string>("Hello");
// str 的类型自动推断为 string,拥有完整的智能提示
str.toUpperCase(); // ✅ OK
// 调用时明确指定 T 为 number
const num = identity<number>(123);
// num 的类型自动推断为 number
num.toFixed(); // ✅ OK
// TypeScript 通常能自动推断,甚至可以省略 <string>
const auto = identity("Auto Infer"); // T 自动被推断为 string
2. 泛型的核心语法
A. 基础用法:类型变量 T
T 是 convention(惯例),代表 Type。你也可以用 U, V, K, V (Key, Value) 等,只要成对出现即可。
// 定义一个泛型函数
function createPair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
// 使用
const pair1 = createPair<string, number>("age", 18);
// 类型: [string, number]
const pair2 = createPair(true, "hello");
// 类型自动推断: [boolean, string]
B. 泛型接口 (Interfaces)
接口也可以定义泛型,常用于定义数据结构(如 API 响应、列表项)。
// 定义一个通用的盒子结构
interface Box<T> {
content: T;
}
const stringBox: Box<string> = { content: "Hello" };
const numberBox: Box<number> = { content: 100 };
// 泛型接口还可以有多个参数
interface Map<K, V> {
key: K;
value: V;
}
const userMap: Map<string, { id: number; name: string }> = {
key: "user_1",
value: { id: 1, name: "Alice" }
};
C. 泛型类 (Classes)
类也可以使用泛型,确保类内部的方法和属性类型一致。
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
// numberStack.push("error"); // ❌ 报错:不能推入字符串
const top = numberStack.pop(); // top 的类型是 number | undefined
3. 高级泛型技巧
A. 泛型约束 (Constraints) - extends
有时候我们希望泛型 T 必须是某种特定的类型(比如必须包含 length 属性),这时可以使用 extends 关键字。
// 错误示范:T 可能是 number,number 没有 .length 属性
// function loggingIdentity<T>(arg: T): T {
// console.log(arg.length); // ❌ 报错
// return arg;
// }
// 正确示范:约束 T 必须具有 length 属性
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // ✅ 现在 TS 知道 arg 一定有 length
return arg;
}
loggingIdentity("hello"); // ✅ string 有 length
loggingIdentity([1, 2, 3]); // ✅ array 有 length
loggingIdentity({ length: 10, width: 5 }); // ✅ 对象有 length
// loggingIdentity(123); // ❌ 报错:number 没有 length 属性
B. 在泛型约束中使用类型参数
你可以用一个类型参数去约束另一个类型参数。
// K 必须是 obj 的键名之一 (keyof T)
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
const user = { id: 1, name: "Bob", active: true };
const id = getProperty(user, "id"); // ✅ 类型: number
const name = getProperty(user, "name"); // ✅ 类型: string
// const email = getProperty(user, "email"); // ❌ 报错:"email" 不是 user 的键
C. 默认泛型类型
可以为泛型指定默认值,如果调用时没传,就用默认值。
interface Response<T = string> {
data: T;
status: number;
}
// 不传泛型参数,T 默认为 string
const res1: Response = { data: "ok", status: 200 };
// 传入泛型参数,覆盖默认值
const res2: Response<{ id: number }> = { data: { id: 1 }, status: 200 };
4. 常见内置泛型工具类型
TypeScript 内置了许多实用的泛型工具类型,日常开发非常常用:
| 工具类型 | 作用 | 示例 |
|---|---|---|
Partial<T> | 将 T 的所有属性变为可选 | Partial<User> |
Required<T> | 将 T 的所有属性变为必填 | Required<User> |
Readonly<T> | 将 T 的所有属性变为只读 | Readonly<User> |
Pick<T, K> | 从 T 中选取一组属性 K | Pick<User, 'id' | 'name> |
Omit<T, K> | 从 T 中剔除一组属性 K | Omit<User, "password"> |
Record<K, T> | 构造一个键类型为 K,值为 T 的对象 | Record<string, number> |
ReturnType<T> | 获取函数 T 的返回值类型 | ReturnType<typeof myFunc> |
Promise<T> | 表示一个解析为 T 的 Promise | Promise<string> |
interface User {
id: number;
name: string;
email: string;
}
// 更新用户时,ID 和名字通常不变,只需要部分字段
type UpdateUserDto = Partial<Omit<User, 'id'>>;
// 等价于: { name?: string; email?: string; }