泛型(generics)是把类型变成参数。前面所有章节的类型都是具体的——string、number、{ x: number }——泛型解决的问题是:当类型本身事先未知时,怎么让类型关系在调用点才确定下来。
这不是"为了少写几个类型",而是为了表达"输入和输出之间的关系"。
为什么需要泛型
先看不使用泛型的做法。一个取数组首元素的函数,要为每种类型写一遍:
1function firstString(arr: string[]): string | undefined { return arr[0]; }
2function firstNumber(arr: number[]): number | undefined { return arr[0]; }
3function firstBool(arr: boolean[]): boolean | undefined { return arr[0]; }
用联合类型可以少写几个,但会丢失类型关系:
1function first(arr: unknown[]): unknown { return arr[0]; }
2const x = first([1, 2, 3]); // x 是 unknown,不是 number
返回类型 unknown 和输入类型 number[] 之间的关系丢失了。泛型把这条关系写出来:
1function first<T>(arr: T[]): T | undefined { return arr[0]; }
2const x = first([1, 2, 3]); // x 是 number | undefined
T 表示"调用时才知道的那个类型",arr: T[] 和 : T 之间的对应关系被类型检查器记住了。
泛型函数、接口、类
1// 泛型函数:类型作为参数
2function identity<T>(value: T): T {
3 return value;
4}
5
6// 显式指定
7const a = identity<string>("hello"); // string
8
9// 自动推断
10const b = identity(42); // number
11
12// 多个类型参数
13function pair<K, V>(k: K, v: V): [K, V] {
14 return [k, v];
15}
16
17// 泛型约束:T 必须有 length
18function logLength<T extends { length: number }>(x: T): number {
19 return x.length;
20}
21
22// keyof 约束:K 必须是 T 的键
23function get<T, K extends keyof T>(obj: T, key: K): T[K] {
24 return obj[key];
25}
26
27// 默认类型参数
28interface Box<T = string> {
29 value: T;
30}
31
32const box1: Box = { value: "a" }; // T 默认 string
33const box2: Box<number> = { value: 1 };
34
35// 泛型类
36class Stack<T> {
37 private items: T[] = [];
38 push(item: T): void { this.items.push(item); }
39 pop(): T | undefined { return this.items.pop(); }
40 get size(): number { return this.items.length; }
41}
42
43const s = new Stack<number>();
44s.push(1);
45s.push(2);
46
47console.log(a, b, pair("k", 1), logLength("abc"), logLength([1, 2]));
48console.log(get({ x: 1, y: "a" }, "y"), box1.value, box2.value, s.size, s.pop());
1hello 42 [ 'k', 1 ] 3 2
2a a 1 2 2
泛型函数
1function identity<T>(value: T): T { return value; }
2
3identity<string>("hello"); // 显式指定 T
4identity(42); // 自动推断 T = number
显式指定通常不必要——推断能处理绝大多数情况,而显式指定反而可能写错。只在推断失败或推断结果不符合预期时才写。
泛型接口与类型别名
1interface Box<T = string> {
2 value: T;
3}
4
5type Result<T, E = Error> =
6 | { ok: true; value: T }
7 | { ok: false; error: E };
= string 是默认类型参数,不指定时使用。它可以避免调用方被迫写 Box<string> 这种显而易见的情况。
泛型类
1class Stack<T> {
2 private items: T[] = [];
3 push(item: T): void { this.items.push(item); }
4 pop(): T | undefined { return this.items.pop(); }
5}
6
7const s = new Stack<number>(); // T 在这里绑定
8s.push(1);
类型参数绑定在类上(Stack<number>)时,整个实例的生命周期共享同一个 T。如果只需要单个方法泛型,把 <T> 写在方法上,不要写在类上——类级泛型会让 T 在实例创建时就固定,失去灵活性。
泛型约束
无约束的 T 可以是任何类型,所以不能对它做任何假设:
1function bad<T>(x: T): number {
2 return x.length;
3 // error TS2339: Property 'length' does not exist on type 'T'.
4}
要使用 T 的成员,必须约束它:
1// 泛型约束的各种形态
2
3// 1. 对象形状约束
4function getName<T extends { name: string }>(x: T): string {
5 return x.name;
6}
7
8// 2. keyof 约束
9function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
10 return items.map((item) => item[key]);
11}
12
13// 3. 联合约束
14function toId<T extends string | number>(v: T): string {
15 return `id_${v}`;
16}
17
18// 4. 构造函数约束:new (...args) => T
19function create<T>(Ctor: new () => T): T {
20 return new Ctor();
21}
22
23class Widget {
24 kind = "widget";
25}
26
27// 5. 带参数的构造函数约束
28function createWith<T, A extends unknown[]>(
29 Ctor: new (...args: A) => T,
30 ...args: A
31): T {
32 return new Ctor(...args);
33}
34
35class Point {
36 constructor(public x: number, public y: number) {}
37}
38
39// 6. 用约束保留参数的具体类型(避免推断成宽类型)
40function firstElement<T>(arr: [T, ...T[]]): T {
41 return arr[0];
42}
43
44// 7. const 类型参数(TS 5.0+):保留字面量类型
45function tuple<T extends readonly unknown[]>(items: T): T {
46 return items;
47}
48
49// 没有 const:推断成 string[]
50const t1 = tuple(["a", "b"]);
51const probe1: string[] = t1; // 通过,证明是 string[]
52
53// 加 const 后保留字面量元组
54function tupleConst<const T extends readonly unknown[]>(items: T): T {
55 return items;
56}
57const t2 = tupleConst(["a", "b"]); // readonly ["a", "b"]
58// @ts-expect-error TS4104: readonly 不能赋给可变类型,证明 t2 是字面量元组
59const probe2: string[] = t2;
60
61console.log(getName({ name: "a" }), pluck([{ n: 1 }, { n: 2 }], "n"));
62console.log(toId(1), create(Widget).kind, createWith(Point, 1, 2).x);
63console.log(firstElement([1, 2, 3]), t1, t2);
1a [ 1, 2 ]
2id_1 widget 1
31 [ 'a', 'b' ] [ 'a', 'b' ]
约束的五种常见形态:
| 形态 | 写法 | 用途 |
|---|---|---|
| 对象形状 | T extends { name: string } | 要求有某个属性 |
keyof | K extends keyof T | 键必须存在于对象上 |
| 联合 | T extends string | number | 限定取值集合 |
| 构造函数 | new () => T | 传入一个类 |
| 带参数构造函数 | new (...args: A) => T | 传入类并转发参数 |
keyof 约束是最常用的一个
1function get<T, K extends keyof T>(obj: T, key: K): T[K] {
2 return obj[key];
3}
4
5const value = get({ x: 1, y: "a" }, "y"); // 类型是 string
K extends keyof T 保证 key 一定是 obj 的键,T[K] 是索引访问类型,返回精确的值的类型。这样拼错键名会在编译期报错:
1get({ x: 1, y: "a" }, "z");
2// error TS2345: Argument of type '"z"' is not assignable to parameter of type '"x" | "y"'.
const 类型参数(TS 5.0+)
普通推断会把数组字面量推成 string[],加上 const 修饰符后保留字面量类型:
1function tuple<T extends readonly unknown[]>(items: T): T { return items; }
2function tupleConst<const T extends readonly unknown[]>(items: T): T { return items; }
3
4const t1 = tuple(["a", "b"]);
5const probe1: string[] = t1; // 通过,说明 t1 是 string[]
6
7const t2 = tupleConst(["a", "b"]); // readonly ["a", "b"]
8// @ts-expect-error TS4104: readonly 不能赋给可变类型
9const probe2: string[] = t2;
实测错误消息,证明 t2 保留了字面量元组类型:
1error TS4104: The type 'readonly ["a", "b"]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.
const T 是给"类型推断太宽"这个问题提供的官方解法,比自己写 as const 更自然。
推断时机与位置
类型参数从哪些位置被推断出来,是理解泛型行为的关键:
1// 泛型的推断位置:类型参数从哪些地方被推断出来
2
3// 1. 从参数推断(最常见)
4function fromParam<T>(x: T): T { return x; }
5const r1 = fromParam("a"); // T = string
6
7// 2. 从参数的结构推断
8function fromProp<T>(obj: { value: T }): T { return obj.value; }
9const r2 = fromProp({ value: 1 }); // T = number
10
11// 3. 从数组元素推断
12function fromArray<T>(arr: T[]): T | undefined { return arr[0]; }
13const r3 = fromArray([1, 2]); // T = number
14
15// 4. 从回调参数推断(参数位置逆变,推断较弱)
16function fromCallback<T>(cb: () => T): T { return cb(); }
17const r4 = fromCallback(() => 1); // T = number
18
19// 5. 推断不出来的情况:类型参数只出现在返回位置
20function make<T>(): T {
21 return undefined as T;
22}
23// 不指定类型参数时,T 推断为 unknown(不是 any,也不是报错)
24const uninferred = make();
25// @ts-expect-error TS2322: unknown 不能赋给 string
26const probe: string = uninferred;
27const r5 = make<string>(); // 必须显式指定才能拿到具体类型
28
29// 6. 从多个位置推断,取"最佳公共类型"
30function combine<T>(a: T, b: T): T { return a; }
31const r6 = combine(1, 2); // T = number
32const r7 = combine("a", "b"); // T = string
33// const r8 = combine(1, "b"); // 报错:推断成 string | number 后仍不匹配
34
35// 7. 默认类型参数:推断失败时使用
36function withDefault<T = string>(x?: T): T | undefined { return x; }
37const r9 = withDefault(); // T = string
38const r10 = withDefault(1); // T = number
39
40// 8. NoInfer(TS 5.4+):阻止从某位置推断
41function withNoInfer<T>(items: T[], fallback: NoInfer<T>): T {
42 return items.length ? items[0]! : fallback;
43}
44// fallback 不参与推断,T 由 items 决定
45const r11 = withNoInfer([1, 2], 0); // T = number
46
47console.log(r1, r2, r3, r4, r5, r6, r7, r9, r10, r11);
1a 1 1 1 undefined 1 a undefined 1 1
| 推断来源 | 例子 | 结果 |
|---|---|---|
| 参数本身 | f<T>(x: T) | f("a") → T = string |
| 参数的结构 | f<T>(o: { value: T }) | f({value: 1}) → T = number |
| 数组元素 | f<T>(arr: T[]) | f([1,2]) → T = number |
| 回调返回值 | f<T>(cb: () => T) | f(() => 1) → T = number |
| 默认参数 | f<T = string>(x?: T) | f() → T = string |
推断不出来的情况
如果类型参数只出现在返回位置,就没有东西可以推断:
1function make<T>(): T {
2 return undefined as T;
3}
4
5const uninferred = make(); // T = unknown
6// @ts-expect-error TS2322: unknown 不能赋给 string
7const probe: string = uninferred;
8
9const r5 = make<string>(); // 必须显式指定
注意结果是 unknown 而不是 any,也不是报错。这是有意的设计:unknown 强制你显式收窄(见 类型收窄),而 any 会静默传播。
NoInfer(TS 5.4+)
有些位置你不希望参与推断:
1function withNoInfer<T>(items: T[], fallback: NoInfer<T>): T {
2 return items.length ? items[0]! : fallback;
3}
4
5const r11 = withNoInfer([1, 2], 0); // T = number,由 items 决定
没有 NoInfer 时,fallback 也会参与推断,可能把 T 推成一个不该出现的联合类型。
类型参数的位置决定作用域
<T> 写在哪里,决定了它什么时候被绑定:
1// T 在调用签名里 → 每次调用函数时绑定
2type Filter1 = { <T>(arr: T[], f: (x: T) => boolean): T[] };
3
4// T 在类型别名上 → 使用 Filter2<number> 时绑定一次
5type Filter2<T> = { (arr: T[], f: (x: T) => boolean): T[] };
6
7// 具名函数:每次调用绑定
8function filter<T>(arr: T[], f: (x: T) => boolean): T[] { /* ... */ }
第一种更灵活(一个变量可以依次处理不同类型),第二种更简单(写一次约束到底)。选择取决于你是否需要同一个函数变量处理多种类型。
常见误区
误区一:给泛型加不必要的约束
1// 没必要:T 没有被真正约束,只是限制了调用方
2function bad<T extends object>(x: T): T { return x; }
3
4// 需要时才约束
5function good<T extends { length: number }>(x: T): number { return x.length; }
extends object 没有给你任何额外的能力,只是把 string、number 等调用方排除掉了。
误区二:把泛型当 any 用
1// 这是泛型语法,但等于 any
2function bad<T>(x: T): T {
3 return JSON.parse(JSON.stringify(x));
4}
T 没有任何约束时,你不能对 x 做任何有意义的操作。如果函数体不需要用到 T 的任何能力,那它可能不需要泛型。
误区三:忽略 T 可能推断成 never
1function first<T>(arr: T[]): T | undefined { return arr[0]; }
2const r = first([]); // T 推断为 never
实测:T 确实是 never,而返回类型 never | undefined 会被简化为 undefined:
1const a: never = first([])!; // 通过,证明 T 是 never
2const b: undefined = r; // 通过,证明返回类型就是 undefined
3const c: string = r;
4// error TS2322: Type 'undefined' is not assignable to type 'string'.
空数组让 TypeScript 无法确定元素类型,推断为 never。这时显式指定类型参数:first<number>([])。
小结
| 主题 | 核心结论 |
|---|---|
| 泛型的作用 | 表达输入与输出之间的类型关系,不是少写代码 |
| 类型参数位置 | 写在类上 = 实例级绑定;写在方法上 = 调用级绑定 |
| 约束 | 要用 T 的成员就必须约束;K extends keyof T 最常用 |
| 推断来源 | 参数、参数结构、数组元素、回调返回值、默认值 |
| 推断失败 | 结果是 unknown,需要显式指定或收窄 |
const T | 保留字面量类型,替代 as const |
NoInfer | 阻止某个位置参与推断 |
下一篇:高级类型。