泛型

2026-09-09 00:00    #typescript  

泛型(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);
泛型类 vs 泛型构造函数

类型参数绑定在类上(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 }要求有某个属性
keyofK 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阻止某个位置参与推断

下一篇:高级类型。