TypeScript 泛型是什么?为什么要用泛型?
简化版
泛型是类型层面的参数化,让函数、接口、类在保持类型安全的同时复用逻辑。它能把输入类型和输出类型关联起来,避免用 any 丢失类型信息。
详细版
示例:
function identity<T>(value: T): T {
return value;
}
const n = identity(1); // number
const s = identity('a'); // string
如果用 any:
function identity(value: any): any {
return value;
}
返回值类型也变成 any,调用方失去提示。泛型保留了类型关系。
泛型还能加约束:
function getLength<T extends { length: number }>(value: T) {
return value.length;
}
完整版教学
一、泛型解决“类型也要复用”的问题
普通函数复用值逻辑,泛型复用类型逻辑。比如数组取第一项,输入是 number[] 返回 number,输入是 User[] 返回 User。
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
没有泛型就很难表达这种关系。
二、泛型参数由调用推断
很多时候不需要手写类型参数:
first([1, 2, 3]);
TS 会推断 T 是 number。只有推断不出来或需要指定更宽类型时,才显式写:
first<string | number>([1, 'a']);
三、泛型约束
没有约束的 T 可以是任何类型,所以不能随便访问属性:
function f<T>(x: T) {
// x.length; // 报错
}
用 extends 约束后,TS 才知道它至少有某些能力。
四、常见应用
泛型常用于接口响应:
interface ApiResponse<T> {
code: number;
data: T;
}
也用于组件 props、表格列配置、状态管理、工具函数等。它的核心价值是让复用代码不丢类型。
五、面试追问与工程落地
泛型常见追问是“泛型和 any 有什么本质区别”。any 是放弃类型关系,泛型是保留类型关系。function first<T>(arr: T[]): T 能保证传 number[] 返回 number;any 只能告诉你“随便是什么”,调用方失去约束和提示。
还会追问泛型默认值和多泛型。比如组件 props、请求函数中常写 TData = unknown,让调用方可以不传类型参数,也能有安全默认值。多个泛型之间可以互相约束,如 K extends keyof T,表达 key 和对象的关系。
工程里泛型不要为了炫技过度抽象。如果一个函数没有真实类型关系,只是把所有地方都写成 T,会让调用更复杂。好泛型应该减少重复、增强约束;坏泛型只是把简单问题变难。
六、从推断候选到约束检查理解泛型调用
泛型调用不是简单地把实参类型复制到 T。编译器会从参数、上下文返回类型等位置收集候选,再选择满足约束的类型;显式类型参数则会跳过部分推断,但仍必须通过 extends 约束。泛型默认值只在无法或无需提供类型参数时给出后备,不会覆盖已经推断出的候选。
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = { id: 1, name: 'Ada' }
const id = get(user, 'id') // number
const name = get(user, 'name') // string
// get(user, 'age') // 报错
| 写法 | 类型关系 | 调用结果 |
|---|---|---|
(value: any) => any | 输入输出断开 | 返回 any |
<T>(value: T) => T | 输入输出同一类型参数 | 保留实参类型 |
<T extends X>(value: T) | 保留具体类型且保证至少满足 X | 可安全使用 X 的能力 |
<T = unknown>() | 没有候选时提供默认值 | 安全后备类型 |
上例对象有 2 个键,因此 K 只能从 'id' | 'name' 中选择;选择 'id' 后,索引访问类型 T[K] 精确得到 number。若把 key 只写成 string,这种键和值之间的关联就丢失了。
好泛型必须表达真实关系:同一个
T至少应连接两个位置,或者用约束把两个类型参数关联起来;孤立的泛型往往只是增加阅读成本。
七、常见误区与追问
- 误区:泛型就是类型安全的 any。 any 放弃关系检查,泛型通过同一类型参数保存输入、输出或成员之间的关系。
- 误区:使用泛型时总要显式写尖括号。 大多数函数调用可以从实参或上下文推断,显式参数只在推断不足或需要主动拓宽时使用。
- 误区:T extends X 会把 T 固定成 X。 它只规定 T 至少满足 X,返回值仍可保留调用方传入的更具体类型。
- 追问:为什么泛型约束常写 K extends keyof T? 它保证 K 是 T 的合法键,使
T[K]能安全表达对应属性值类型。 - 追问:泛型默认值什么时候生效? 当类型参数可选且没有推断候选或调用方没有提供时使用,前面的必填类型参数也要满足声明顺序规则。
- 追问:泛型在运行时还存在吗? 普通 TypeScript 泛型会被擦除,不能在运行时通过
T做instanceof或创建new T()。 - 追问:什么时候不该使用泛型? 当类型参数只出现一次、没有连接任何类型关系时,直接写具体类型或联合类型通常更清楚。
八、加强记忆
泛型就是“把类型当参数传”。它不是为了复杂,而是为了表达输入和输出的类型关系:逻辑复用,类型不丢。
最后检查类型参数是否连接了至少两个位置;能保留或约束关系才是泛型存在的理由。