理解"类型参数从哪里被推断出来",能解释绝大多数"为什么这里推断成了 unknown“的困惑。
会被推断的位置
1// 泛型推断位置:类型参数从哪些地方被推断出来
2
3// ---- 会被推断的位置 ----
4
5// 1. 参数类型
6function fromParam<T>(x: T): T { return x; }
7const a = fromParam("a"); // T = string
8
9// 2. 参数的结构
10function fromProp<T>(o: { value: T }): T { return o.value; }
11const b = fromProp({ value: 1 }); // T = number
12
13// 3. 数组元素
14function fromArray<T>(arr: T[]): T | undefined { return arr[0]; }
15const c = fromArray([1, 2]); // T = number
16
17// 4. 元组位置
18function fromTuple<T, U>(pair: [T, U]): [U, T] { return [pair[1], pair[0]]; }
19const d = fromTuple(["a", 1]); // T = string, U = number
20
21// 5. 回调返回值
22function fromCallback<T>(cb: () => T): T { return cb(); }
23const e = fromCallback(() => 1); // T = number
24
25// 6. 嵌套结构
26function fromNested<T>(o: { a: { b: T } }): T { return o.a.b; }
27const f = fromNested({ a: { b: true } }); // T = boolean
28
29// 7. 索引访问位置
30function fromIndex<T, K extends keyof T>(o: T, k: K): T[K] { return o[k]; }
31const g = fromIndex({ x: 1 }, "x"); // T = { x: number }, K = "x"
32
33// ---- 不会被推断的位置 ----
34
35// 1. 只在返回位置出现
36function make<T>(): T { return undefined as T; }
37const h = make(); // T = unknown(不是推断出来的)
38
39// 2. 只在约束里出现(U extends T,但 T 由第一个参数推断)
40function constrained<T, U extends T>(a: T, b: U): T { return a; }
41// 注意:T 被推断为字面量 1,所以 2 不满足 U extends 1
42const i = constrained<number, number>(1, 2);
43
44// 3. 只在条件类型里出现
45type Unwrap<T> = T extends Promise<infer U> ? U : T;
46// 这里的 U 是 infer 声明的,不是函数类型参数
47
48// ---- 多位置推断:取"最佳公共类型" ----
49
50function combine<T>(a: T, b: T): T { return a; }
51const j = combine(1, 2); // T = number
52const k = combine("a", "b"); // T = string
53// combine(1, "a") 会报错:T 推断为 string | number,但参数各自不匹配
54
55// 用元组可以避免"公共类型"问题
56function combine2<T, U>(a: T, b: U): [T, U] { return [a, b]; }
57const l = combine2(1, "a"); // T = number, U = string
58
59// ---- 默认类型参数:推断失败时使用 ----
60function withDefault<T = string>(): T | undefined { return undefined; }
61const m = withDefault(); // T = string(默认值)
62const n = withDefault<number>(); // T = number
63
64// ---- NoInfer:阻止某位置参与推断(TS 5.4+)----
65function withNoInfer<T>(items: T[], fallback: NoInfer<T>): T {
66 return items.length ? items[0]! : fallback;
67}
68const o = withNoInfer([1, 2], 0); // T = number,由 items 决定
69
70// 对比:不加 NoInfer 时 fallback 也参与推断
71function withoutNoInfer<T>(items: T[], fallback: T): T {
72 return items.length ? items[0]! : fallback;
73}
74const p = withoutNoInfer([1, 2], 0); // T = number(这里恰好一致)
75
76// 没有 NoInfer 时,fallback 会影响推断结果:
77// 传入不匹配的值会报错,而不是静默扩大 T
78// @ts-expect-error TS2345: T 已由 items 确定为 number
79const pBad = withoutNoInfer([1, 2], "x");
80
81// 想接受混合类型要显式标注
82const pMixed = withoutNoInfer<number | string>([1, 2], "x");
83
84// ---- 推断失败时的行为:unknown,不是 any ----
85function onlyReturn<T>(): T { return undefined as T; }
86const q = onlyReturn();
87// @ts-expect-error TS2322: T 推断为 unknown
88const qCheck: string = q;
89
90// 显式指定才能拿到具体类型
91const r = onlyReturn<string>();
92
93console.log(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, pBad, pMixed, qCheck, r);
1a 1 1 [ 1, 'a' ] 1 true 1 undefined 1 1 a [ 1, 'a' ] undefined undefined 1 1 undefined undefined
| 位置 | 例子 | 推断结果 |
|---|---|---|
| 参数本身 | f<T>(x: T) → f("a") | T = string |
| 参数的结构 | f<T>(o: { value: T }) | T = number |
| 数组元素 | f<T>(arr: T[]) | T = number |
| 元组位置 | f<T, U>(p: [T, U]) | 按位置对应 |
| 回调返回值 | f<T>(cb: () => T) | T = number |
| 嵌套结构 | f<T>(o: { a: { b: T } }) | T = boolean |
| 索引访问 | f<T, K extends keyof T>(o, k) | T 和 K 分别推断 |
共同点:类型参数出现在参数的类型里,调用时就能从实参反推。
不会被推断的位置
只在返回位置出现
1function make<T>(): T { return undefined as T; }
2
3const h = make(); // T = unknown
没有任何参数提供信息,TypeScript 无法推断。结果是 unknown——不是 any,也不是报错:
1const q = onlyReturn();
2const qCheck: string = q;
3// error TS2322: Type 'unknown' is not assignable to type 'string'.
unknown 的设计意图是强制你显式处理(见 类型收窄)。要拿具体类型必须显式指定:
1const r = onlyReturn<string>(); // T = string
只在约束里出现
1function constrained<T, U extends T>(a: T, b: U): T { return a; }
T 从 a 推断,U 从 b 推断。看起来没问题,但有个陷阱:
1constrained(1, 2);
2// error TS2345: Argument of type '2' is not assignable to parameter of type '1'.
T 被推断为字面量 1(因为 const 风格的推断),所以 U extends 1 只接受 1。需要显式放宽:
1constrained<number, number>(1, 2); // ✓
多位置推断:取最佳公共类型
同一个类型参数出现在多个参数位置时,TypeScript 会找"最佳公共类型”:
1function combine<T>(a: T, b: T): T { return a; }
2
3combine(1, 2); // T = number
4combine("a", "b"); // T = string
5combine(1, "a"); // 报错
combine(1, "a") 会尝试 T = string | number,但那时 a: number 和 b: string 都不满足 T。
用独立类型参数避免这个问题
如果两个参数类型确实可以不同,用两个类型参数:
1function combine2<T, U>(a: T, b: U): [T, U] { return [a, b]; }
2const l = combine2(1, "a"); // T = number, U = string
默认类型参数
推断失败时的兜底:
1function withDefault<T = string>(): T | undefined { return undefined; }
2
3withDefault(); // T = string(用默认值)
4withDefault<number>(); // T = number(显式指定优先)
NoInfer:阻止某位置参与推断(TS 5.4+)
有些位置你不希望它影响推断结果:
1function withNoInfer<T>(items: T[], fallback: NoInfer<T>): T {
2 return items.length ? items[0]! : fallback;
3}
4
5withNoInfer([1, 2], 0); // T = number,由 items 决定
没有 NoInfer 时,fallback 也参与推断——它会让不匹配的值直接报错,而不是静默扩大 T:
1function withoutNoInfer<T>(items: T[], fallback: T): T { }
2
3withoutNoInfer([1, 2], "x");
4// error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
5
6// 想接受混合类型要显式标注
7withoutNoInfer<number | string>([1, 2], "x"); // ✓
NoInfer<T> 的语义是"这个位置使用 T,但不推断 T"。它适合的场景是:某个参数的类型应该完全跟随另一个参数,而不应该反过来影响它。
推断规则速查
| 想做的事 | 写法 |
|---|---|
| 从参数推断 | f<T>(x: T) |
| 保留字面量 | f<const T>(x: T)(见 泛型) |
| 阻止某位置推断 | NoInfer<T> |
| 提供兜底 | f<T = string>(...) |
| 只在返回位置用 | 调用方必须显式指定 f<string>() |