什么是泛型?如何使用?
下文默认基于 TypeScript 5.x,并假设开启
strict: true。
面试速答(30 秒版 TL;DR)
- 泛型(Generics)就是把“类型”也当成参数,让同一套逻辑适配多种具体类型。
- 它解决的核心问题是:既要复用代码,又要保留输入和输出之间的类型关系。
- 最典型的写法是:
function identity<T>(value: T): T {
return value;
}
- 面试里一句话总结:
any是直接放弃检查;- 泛型是先不写死类型,但保留类型约束和推导能力。
心智模型:把“变化的类型”提成参数
1. 为什么需要泛型
1.1 不用泛型,类型关系很容易丢
function getFirst(arr: any[]) {
return arr[0];
}
const result = getFirst(["a", "b"]); // any
问题不是代码不能跑,而是:
- 输入明明是字符串数组;
- 输出明明应该是字符串;
- 结果却退化成了
any。
1.2 用泛型后,类型会随着输入一起流动
function getFirst<T>(arr: T[]): T {
return arr[0];
}
const a = getFirst(["a", "b"]); // string
const b = getFirst([1, 2, 3]); // number
这里最关键的是:
- 参数用了
T[]; - 返回值也用了
T; - 编译器就能知道“输入是什么,输出也该跟着是什么”。
2. 泛型最常出现在哪些位置
2.1 泛型函数
function identity<T>(value: T): T {
return value;
}
这是最基础、最高频的形式。
2.2 泛型接口
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
const userRes: ApiResponse<{ id: number; name: string }> = {
code: 0,
message: "ok",
data: { id: 1, name: "Tom" },
};
适合描述:
- 接口返回结构;
- 分页数据;
- 通用状态容器。
2.3 泛型类型别名
type Nullable<T> = T | null;
type Box<T> = { value: T };
类型别名常和联合类型、条件类型、映射类型配合使用。
2.4 泛型类
class Queue<T> {
private items: T[] = [];
push(item: T) {
this.items.push(item);
}
shift(): T | undefined {
return this.items.shift();
}
}
这类容器模型很适合用泛型表达。
3. 泛型约束怎么用
不是所有 T 都能随便访问属性:
function getLength<T>(value: T) {
return value.length; // 报错
}
如果你要访问 length,就必须告诉编译器这个 T 至少有什么能力:
type HasLength = {
length: number;
};
function getLength<T extends HasLength>(value: T) {
return value.length;
}
此时 T extends HasLength 的意思是:
T不再是任意类型;- 而是至少拥有
length: number的类型。
4. 泛型和 keyof 的经典组合
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Tom" };
const name = getProp(user, "name"); // string
这道题是面试高频题,答法可以拆成三句:
T表示对象整体类型;K extends keyof T保证 key 必须合法;T[K]表示该 key 对应的属性值类型。
5. 泛型参数可以有多个,也可以有默认值
type Result<T, E = Error> =
| { ok: true; data: T }
| { ok: false; error: E };
这说明泛型不是只能写一个 T,而是可以:
- 多个类型参数协作;
- 给部分参数设置默认类型。
6. 泛型通常可以自动推导
很多时候不用手写 <T>:
function wrap<T>(value: T) {
return { value };
}
const result = wrap("hello");
这里 T 会自动推导成 string。
只有在这些场景,才更常需要显式写类型参数:
- 推导结果不够准;
- 需要主动约束调用方式;
- 某些 API 设计上要求调用方传入明确类型。
7. 泛型和联合类型、any 有什么区别
7.1 和联合类型的区别
- 联合类型描述“已知几种可能”;
- 泛型描述“同一套规则适用于很多类型”。
function first(arr: string[] | number[]) {
return arr[0];
}
这不是泛型,它只能处理少数几个固定分支。
7.2 和 any 的区别
any:放弃类型检查;- 泛型:延迟确定具体类型,但不丢掉类型关系。
function identityAny(value: any): any {
return value;
}
function identityGeneric<T>(value: T): T {
return value;
}
后者明显更安全,也更利于推导。
8. 高频面试题标准答法
8.1 什么是泛型
泛型就是把类型参数化,让同一份逻辑在不同类型下复用,同时保持类型信息不丢。
8.2 泛型的核心价值是什么
三个词就够:
- 复用;
- 精确;
- 可推导。
8.3 泛型是不是越多越好
不是。
好的泛型设计应该满足:
- 类型参数真的有意义;
- 参数之间存在清晰关系;
- 调用方大多数时候不用显式手写一长串类型参数。
如果一个 API 需要用户手写一堆 <T, U, K, R, P> 才能用,通常说明设计已经开始失控。
9. 常见误区
- 把泛型当成
any的高级写法。泛型不是放弃检查,而是延迟具体化。 - 类型参数只出现一次,却硬上泛型。这种设计很多时候没有价值。
- 忘了加约束,就直接访问
T上的属性。 - 以为泛型能做运行时类型判断。实际上运行时拿不到
T。
速记要点
- 泛型 = 类型参数化。
- 目标:复用逻辑,同时保留类型关系。
- 常见位置:函数、接口、类型别名、类。
- 常见组合:
extends、keyof、条件类型。