泛型基础与应用场景深度解析
泛型是类型的'函数参数',把类型也变成可传递的变量,一份代码处理多种类型且不丢失精度。 面试能否写出带泛型的工具函数,直接决定对你 TS 水平的定位。
一句话概括
泛型 = 类型的”函数参数”——把类型也变成可传递的变量,一份代码处理多种类型而不丢失精度。TypeScript 面试中,能不能写出带泛型的工具函数,直接决定了面试官对你 TS 水平的定位。
核心知识点
1. 泛型函数:让类型”流过”函数不丢失
1
2
3
4
5
6
7
8
9
10
// ❌ any 方案:入参是 string,返回值丢了类型信息
function identity(x: any): any { return x; }
const a = identity("hello");
a.toUpperCase(); // 不报错,但 a 的类型是 any,没有智能提示
// ✅ 泛型:调用时自动推断,返回值精准保留
function identity<T>(x: T): T { return x; }
const b = identity("hello"); // b: "hello"(字面量类型!)
const c = identity(42); // c: 42
const d = identity([1, 2, 3]); // d: number[]
面试要点:TypeScript 的类型推断比你想象的更激进——传字面量推断为字面量类型,传对象推断为对象字面量类型。如果你期望更宽的类型(如 string 而非 "hello"),手动标注泛型参数:identity<string>("hello")。
2. 泛型约束 extends:不只是”限制”,更是”赋予能力”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 问题:想访问 T 的某个属性,但 TS 不知道 T 有
function getLength<T>(x: T): number {
// return x.length; // ❌ T 上不存在 length
}
// 解决:extends 让 T 获得被约束类型的所有属性
function getLength<T extends { length: number }>(x: T): number {
return x.length; // ✅ TS 知道 T 一定有 length
}
getLength("hello"); // 5
getLength([1, 2, 3]); // 3
getLength({ length: 10 }); // 10
// getLength(42); // ❌ number 没有 length
约束的核心价值不是”限制能传什么”,而是”告诉 TS 这个泛型参数拥有哪些能力”——extends 之后,TS 就知道 T 上有 length,你才能在函数体内安全访问它。
3. 泛型接口:API 响应层的标准范式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 后端统一响应格式 → 前端用泛型接口建模,data 字段自动跟随业务类型
interface ApiResponse<T> {
code: number;
data: T;
message: string;
}
interface User { id: number; name: string; email: string; }
interface Order { orderId: string; amount: number; }
declare function fetchUser(): Promise<ApiResponse<User>>;
declare function fetchOrder(): Promise<ApiResponse<Order>>;
const userRes = await fetchUser();
userRes.data.email; // ✅ 自动补全,类型为 string
const orderRes = await fetchOrder();
orderRes.data.amount; // ✅ 自动补全,类型为 number
4. 泛型 + keyof:编译期保障属性名不写错
1
2
3
4
5
6
7
8
9
10
// 不用 any、不用索引签名,依然能访问任意属性
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Alice", isActive: true } as const;
getProperty(user, "name"); // 返回 "Alice",类型为 "Alice"
getProperty(user, "isActive"); // 返回 true,类型为 true
// getProperty(user, "email"); // ❌ 编译报错——不存在就是不存在
5. 泛型类与泛型 Hook
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 泛型类:类型安全的数据容器
class DataCache<T> {
private store = new Map<string, T>();
set(key: string, value: T): void {
this.store.set(key, value);
}
get(key: string): T | undefined {
return this.store.get(key);
}
}
const userCache = new DataCache<User>();
userCache.set("alice", { id: 1, name: "Alice", email: "a@b.com" });
const u = userCache.get("alice"); // User | undefined ✅
// 泛型 Hook:localStorage 读写自动跟进类型
function useStoredState<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
try {
const item = localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initial;
} catch {
return initial;
}
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}
其实你每天都在用
useState<User | null>(null):那个尖括号<>就是泛型,告诉 TS state 是User | nullaxios.get<User[]>(url):返回值从any变成User[],IDE 里.data[0].name一路补全Array.prototype.map<U>():[1,2,3].map(String)返回string[],因为 map 的签名是map<U>(callback: (v: T) => U): U[]- Ant Design 表格:
const columns: ColumnsType<User>[] = [...]→dataIndex只能是User的 key,写错立即红线 Promise.all<T[]>():Promise.all([fetchUser(), fetchCount()])自动推导出[User, number]
常见误解(FAQ)
❌ 误区 1:「any 和泛型差不多,只是泛型更优雅」
差远了。any 是”关掉类型检查”,泛型是”推迟类型确定但最终保证安全”。function echo(x: any): any 的调用方拿到的永远是 any——编辑器不补全、重构不报错;而 echo<T>(x: T): T 的调用方拿到的是精确类型,你重构改了 User 的字段名,所有调用处都会亮红线提醒你。
❌ 误区 2:「泛型参数必须显式传 <T>」
多数情况不需要。TS 的类型推断从函数实参自动推导 T——你传 [1, 2, 3],T 就是 number。只有推断失败(空数组 [] 推断为 never[])或你想故意放宽类型时,才显式写 <string | number>([])。
❌ 误区 3:「T extends {} 和 T extends object 意思一样」
{} 匹配除 null / undefined 外的所有类型(包括 string、number 等原始类型);object 只匹配引用类型(对象、数组、函数)。function fn<T extends object>(x: T) 不接受 fn("hello"),后者直接报错。面试把这个讲清楚,TS 水平直接升一档评价。
❌ 误区 4:「泛型编译后还有运行时开销」
TypeScript 的泛型是纯编译时机制,编译后类型全部擦除——泛型类/函数编译出来的 JS 和不写泛型完全一样。这是 TS “零运行时成本” 的设计哲学,和 Java(泛型擦除但有桥方法)或 C#(运行时保留泛型信息)不同。
一句话总结
泛型是 TypeScript 类型系统从”给 JS 贴标签”升级到”类型可编程”的入口——你把 T 想象成类型的参数,把 extends 想象成参数校验,所有泛型工具类型就都通了。