大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
泛型
这一课你将能做到
- 写出带类型参数的泛型函数
<T>,并理解类型参数如何被推断 - 定义泛型接口和泛型类,构建可复用的类型化容器
- 使用泛型约束
extends和默认类型参数,处理「限制但不写死」的场景
为什么要学
前面学的类型都是「具体类型」:string、number、User。但真实世界里大量逻辑是通用的:一个 first() 函数该返回数组第一个元素——数组是 number[] 还是 User[] 无所谓,逻辑都一样。如果给每种数组都写一个版本,就是重复造轮子。
泛型(Generics)就是解决这个问题:把类型当成参数传进函数/接口/类,让同一段逻辑适配任意类型,同时保留类型信息。
没有泛型的 TS,很多代码你只能写成 any[]、Promise<any>、useState<any>,类型系统瞬间退化成摆设。学会泛型,你才真正能用 TS 写出既灵活又安全的代码。
先建立图像
泛型的直觉:函数接收「值参数」,泛型函数还额外接收「类型参数」。
普通函数: function add(a, b) => 传入两个值,返回一个值
泛型函数: function first<T>(arr: T[]) => 传入一个数组 + 一个类型 T
T 由调用方决定
返回类型是 T | undefined
first<number>([1, 2, 3]) T = number, 返回 number | undefined
first<string>(["a", "b"]) T = string, 返回 string | undefined
first([true, false]) 编译器自动推断 T = boolean
跟着做
新建 generics-demo.ts,逐步实操。
步骤 1:第一个泛型函数
// 没有泛型:只能处理 number 数组
function firstNumber(arr: number[]): number | undefined {
return arr[0];
}
// 有泛型:什么数组都能处理
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const n = first<number>([1, 2, 3]); // n: number | undefined
const s = first<string>(["a", "b"]); // s: string | undefined
const b = first([true, false]); // 自动推断 T = boolean
console.log(n, s, b); // 1 "a" true
<T> 就是类型参数(type parameter)。调用时可以不写,TS 会从实参推断。
步骤 2:多个类型参数
// 两个独立的类型参数
function pair<K, V>(key: K, value: V): [K, V] {
return [key, value];
}
const p1 = pair("name", "小明"); // [string, string]
const p2 = pair("age", 25); // [string, number]
console.log(p1, p2); // [ 'name', '小明' ] [ 'age', 25 ]
每个类型参数独立推断,K 和 V 可以相同也可以不同。
步骤 3:泛型约束 extends
类型参数不能乱传,要满足某种「能力」时用 extends 约束:
// 要求 T 必须有 length 属性
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(value: T): T {
console.log(`长度:${value.length}`);
return value;
}
logLength("hello"); // string 有 length,OK
logLength([1, 2, 3]); // array 有 length,OK
// logLength(123); // 报错:number 没有 length
步骤 4:泛型接口
// 通用 API 响应格式
interface ApiResponse<T> {
code: number;
message: string;
data: T; // data 的类型由调用方决定
}
interface UserInfo {
name: string;
email: string;
}
const userResponse: ApiResponse<UserInfo> = {
code: 0,
message: "success",
data: { name: "小明", email: "ming@example.com" },
};
const listResponse: ApiResponse<string[]> = {
code: 0,
message: "success",
data: ["tag1", "tag2"],
};
console.log(userResponse.data.name); // 小明
console.log(listResponse.data); // [ 'tag1', 'tag2' ]
步骤 5:泛型类
class Box<T> {
private content: T;
constructor(value: T) {
this.content = value;
}
get(): T {
return this.content;
}
set(value: T): void {
this.content = value;
}
map<U>(fn: (value: T) => U): Box<U> {
return new Box(fn(this.content));
}
}
const numberBox = new Box<number>(42);
console.log(numberBox.get()); // 42
const stringBox = numberBox.map((n) => `数字是 ${n}`);
console.log(stringBox.get()); // 数字是 42
map<U> 是泛型方法:每次调用时可以有不同的 U,把 Box<number> 变成 Box<string>。
步骤 6:默认类型参数
// T 默认是 string,不传也能用
interface Paginated<T = string> {
items: T[];
total: number;
page: number;
}
const page1: Paginated = { items: ["a", "b"], total: 2, page: 1 };
const page2: Paginated<number> = { items: [1, 2, 3], total: 3, page: 1 };
console.log(page1.items); // [ 'a', 'b' ]
console.log(page2.items); // [ 1, 2, 3 ]
步骤 7:泛型工具函数实战——类型安全的事件系统
type EventMap = {
click: { x: number; y: number };
scroll: { deltaY: number };
keydown: { key: string };
};
class TypedEmitter<Events extends Record<string, unknown>> {
private listeners = new Map<string, ((payload: never) => void)[]>();
on<K extends keyof Events>(
event: K,
handler: (payload: Events[K]) => void
): void {
const list = this.listeners.get(event as string) ?? [];
list.push(handler as (payload: never) => void);
this.listeners.set(event as string, list);
}
emit<K extends keyof Events>(event: K, payload: Events[K]): void {
const list = this.listeners.get(event as string) ?? [];
for (const handler of list) {
handler(payload);
}
}
}
const emitter = new TypedEmitter<EventMap>();
emitter.on("click", (e) => {
console.log(`点击坐标:${e.x}, ${e.y}`); // e 自动推断为 { x: number; y: number }
});
emitter.on("keydown", (e) => {
console.log(`按下键:${e.key}`); // e 自动推断为 { key: string }
});
emitter.emit("click", { x: 100, y: 200 });
emitter.emit("keydown", { key: "Enter" });
这个例子覆盖了泛型的核心用法:约束 extends Record、keyof 索引、泛型事件名映射。理解了它,React/Vue 的类型推导对你就是透明的。
步骤 8:条件类型初探
// 条件类型:T extends U ? X : Y,根据类型关系选择结果
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// 实际用途:提取数组元素类型
type ElementType<T> = T extends (infer U)[] ? U : T;
type E1 = ElementType<string[]>; // string
type E2 = ElementType<number>; // number
console.log("条件类型演示完成");
infer 是「类型推断」关键字:从已知类型中「提取」出未知部分。T extends (infer U)[] 意思是「如果 T 是某种数组,把数组元素类型提取到 U」。
讲清楚要点
- 泛型是类型层面的函数参数:值参数在运行时传入,类型参数在编译时传入。
<T>就是占位符,调用时确定具体类型。 - 能推断就不写:
first([1, 2, 3])比first<number>([1, 2, 3])更简洁。写死类型参数只在推断失败或需要显式收窄时。 extends是约束不是继承:T extends HasLength读作「T 必须满足 HasLength 的形状」,跟 class 的 extends 语义不同。- 泛型类和泛型接口的区别:泛型接口描述形状,编译后消失;泛型类有运行时实体,
new Box<number>(42)和new Box<string>("a")在运行时都是Box的实例。 - 类型参数名是约定:
T= Type,K= Key,V= Value,E= Element,U= 另一个 Type。约定俗成,别瞎起。 - 泛型不承诺运行时类型检查:
Box<string>编译后就是Box,你塞个数字进去,编译器拦得住,运行时(比如从 JSON 反序列化)拦不住。需要运行时校验请结合 zod 等库。
你可能会懵的地方
-
泛型什么时候会「丢」类型? 类型断言
as any、类型断言到错误类型、或者从any/unknown直接赋值时,泛型的保护就失效了。编译器信你,但运行时不一定对。 -
<T,>后面的逗号是什么? 在.tsx文件里,<T>会被解析成 JSX 标签。加逗号<T,>告诉编译器这是泛型不是 JSX。 -
T extends object和T extends {}有区别吗?object排除原始类型;{}在 TS 里几乎等于「非 null/undefined 的任何值」(包括 string、number)。{}是历史遗留的宽松行为,尽量用object或更精确的约束。 -
泛型函数能重载吗? 能。比如
function first<T>(arr: T[]): T | undefined和function first<T>(arr: T[], fallback: T): T是两个重载签名。 -
条件类型里
infer能提取多个吗? 能:T extends [infer A, ...infer Rest]可以提取元组第一个和剩余。这是高阶技巧,日常开发中用内置工具类型(Parameters、ReturnType)就够了。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
到处用 any[] 代替泛型 | 丢失元素类型,push 什么都行 | Array<T> 或 T[] |
| 泛型参数不加约束直接调属性 | T 上不存在属性,编译报错 | T extends HasLength 或先用守卫收窄 |
在 .tsx 里写 <T> 泛型 | 被解析成 JSX 标签报错 | 加逗号 <T,> 或改用函数声明 |
把泛型类当类型用 Box 不带参数 | 类型不完整 | Box<number> 或 Box<typeof value> |
| 误以为泛型有运行时信息 | typeof box 拿不到 Box<number>,只有 Box | 运行时校验用 zod,别靠泛型 |
卡点急救
- 报错
Type 'T' does not satisfy the constraint:类型参数没满足约束。检查传入的类型是否缺少约束要求的属性,或把约束放宽。 .tsx文件里泛型箭头函数报错:<T>被解析成 JSX。改成<T,>或const fn = <T extends unknown>(...) => ...。- 泛型类
new Box()推断不出类型:构造函数参数没有用到T。给构造函数加参数或显式标注new Box<number>(42)。 - 条件类型推不出想要的类型:
infer的位置不对。检查模式匹配的结构是否和实际类型完全对应。
面试常问
-
什么是泛型?解决什么问题? 泛型是类型参数化:把类型作为参数传给函数/接口/类,让同一段逻辑适配多种类型而不丢失类型信息。解决「通用逻辑 + 类型安全」的矛盾。
-
泛型约束
extends的作用? 限制类型参数必须满足某个形状(比如必须有length属性),让函数体内部能安全地访问该属性。没有约束时T只能当不透明类型用。 -
keyof和泛型的关系?keyof取对象类型的所有键组成联合类型,常和泛型配合:function get<T, K extends keyof T>(obj: T, key: K): T[K],实现类型安全的属性访问。 -
泛型和
any的区别?any放弃检查,什么都能传、传完什么都能干;泛型保留类型,传入number[]返回就是number | undefined,类型链路完整。泛型是类型安全的复用,any是无类型的复用。
自测三问
function first<T>(arr: T[]): T | undefined中,<T>、T[]、T | undefined各自代表什么?- 为什么
T extends HasLength之后函数体里就能访问value.length? - 泛型在运行时还存在吗?如果需要运行时类型校验,应该怎么办?
小结一句话
泛型 = 类型参数化:用 <T> 占位,调用时确定具体类型,让通用逻辑保留完整的类型链路——这是 TS 从「能写」到「写好」的分水岭。
延伸
- Handbook: Generics
- 下一课:工具类型——
Partial/Pick/Omit/Record等内置泛型工具,一行代码变换类型形状。