条件类型与 infer

2026-09-09 00:00    #typescript  

条件类型是类型层面的 if,infer 是类型层面的"提取变量"。两者组合出的能力是 TypeScript 类型系统的核心,也是内置工具类型的实现基础。

分布式条件类型

 1// 分布式条件类型:裸类型参数遇联合会分发
 2
 3// 裸类型参数 T → 分发
 4type ToArray<T> = T extends unknown ? T[] : never;
 5type A = ToArray<string | number>;        // string[] | number[]  ← 分发了
 6
 7// 用 [] 包裹阻止分发
 8type ToArrayNoDist<T> = [T] extends [unknown] ? T[] : never;
 9type B = ToArrayNoDist<string | number>;  // (string | number)[]  ← 没分发
10
11// never 在分发时的特殊性
12type C = ToArray<never>;                  // never(空联合,没有成员可分发)
13
14// 只有"裸"类型参数才分发:被包裹或经过运算就不分发
15type Wrapped<T> = [T] extends [unknown] ? T : never;
16type D = Wrapped<string | number>;        // string | number(不分发)
17
18type InTuple<T> = [T] extends [string] ? T[] : never;
19type E = InTuple<string | number>;        // never(联合不满足 [string])
20
21// 实际应用:内置工具类型都靠分发
22type MyExclude<T, U> = T extends U ? never : T;
23type F = MyExclude<"a" | "b" | "c", "a">;   // "b" | "c"
24
25type MyExtract<T, U> = T extends U ? T : never;
26type G = MyExtract<"a" | "b" | "c", "a" | "b">;   // "a" | "b"
27
28type MyNonNullable<T> = T extends null | undefined ? never : T;
29type H = MyNonNullable<string | null | undefined>;   // string
30
31// 分发在联合的每个成员上独立求值
32type IsString<T> = T extends string ? "yes" : "no";
33type I = IsString<string | number>;       // "yes" | "no"  ← 两个结果合并
34
35// 不分发版本
36type IsStringNoDist<T> = [T] extends [string] ? "yes" : "no";
37type J = IsStringNoDist<string | number>; // "no"(整个联合不满足 string)
38
39// 探测技巧
40// 探测 any:any 同时匹配 1 和 0
41type IsAny<T> = 0 extends 1 & T ? true : false;
42type K = IsAny<any>;                      // true
43type L = IsAny<string>;                   // false
44
45// 探测 never:必须用 [] 阻止分发
46type IsNever<T> = [T] extends [never] ? true : false;
47type M = IsNever<never>;                  // true
48// 不加 [] 时:T extends never ? true : false,传入 never 会直接返回 never
49type IsNeverBad<T> = T extends never ? true : false;
50type N = IsNeverBad<never>;               // never(不是 true!)
51
52// 探测 unknown
53type IsUnknown<T> = [unknown] extends [T] ? ([T] extends [unknown] ? true : false) : false;
54type O = IsUnknown<unknown>;              // true
55type P = IsUnknown<string>;               // false
56
57const a: A = [1];
58const b: B = [1, "a"];
59const c: C = undefined as never;
60const d: D = "x";
61const e: E = undefined as never;
62const f: F = "b";
63const g: G = "a";
64const h: H = "s";
65const i: I = "yes";
66const j: J = "no";
67const k: K = true;
68const l: L = false;
69const m: M = true;
70const n: N = undefined as never;
71const o: O = true;
72const p: P = false;
73
74console.log(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p);
1[ 1 ] [ 1, 'a' ] undefined x undefined b a s yes no true false true undefined true false

最容易踩的规则:裸类型参数遇到联合类型时会分发。

1type ToArray<T> = T extends unknown ? T[] : never;
2
3type A = ToArray<string | number>;   // string[] | number[]  ← 分发了

不是 (string | number)[]。分发意味着条件类型在每个成员上独立求值,结果再合并成联合。

用方括号包裹可以阻止分发:

1type ToArrayNoDist<T> = [T] extends [unknown] ? T[] : never;
2
3type B = ToArrayNoDist<string | number>;   // (string | number)[]

never 的特殊性

1type C = ToArray<never>;   // never(不是 never[])

因为 never 是空联合,没有成员可以分发,整个条件类型直接返回 never。

分发的实际影响

内置工具类型都靠分发实现:

1type MyExclude<T, U> = T extends U ? never : T;
2type MyNonNullable<T> = T extends null | undefined ? never : T;

理解分发后,Exclude 和 Extract 的实现就不再神秘。

分发也会带来"意外"的结果:

1type IsString<T> = T extends string ? "yes" : "no";
2
3type I = IsString<string | number>;        // "yes" | "no"  ← 分发成两个结果
4
5type IsStringNoDist<T> = [T] extends [string] ? "yes" : "no";
6type J = IsStringNoDist<string | number>;  // "no"  ← 不分发

类型探测技巧

有些类型无法用 extends 直接判断,需要技巧:

1// 探测 any:any 同时匹配 1 和 0,用交叉类型利用这个特性
2type IsAny<T> = 0 extends 1 & T ? true : false;
3
4// 探测 never:必须用 [] 阻止分发
5type IsNever<T> = [T] extends [never] ? true : false;
6type M = IsNever<never>;   // true
IsNever 不加方括号会失败
1type IsNeverBad<T> = T extends never ? true : false;
2type N = IsNeverBad<never>;   // never,不是 true!

因为传入 never 时触发分发规则,空联合直接返回 never。这是最容易踩的一个坑。

infer 的进阶用法

 1// infer 的进阶用法
 2
 3// ---- 1. infer 的约束(TS 4.7+)----
 4// 约束不满足时该分支不匹配
 5type FirstString<T> = T extends [infer S extends string, ...unknown[]] ? S : never;
 6type A = FirstString<["a", 1]>;      // "a"
 7type B = FirstString<[1, 2]>;        // never(1 不满足 string)
 8
 9// 对比:不加约束时需要额外检查
10type FirstStringLoose<T> = T extends [infer S, ...unknown[]]
11  ? S extends string ? S : never
12  : never;
13type C = FirstStringLoose<["a", 1]>; // "a"
14type D = FirstStringLoose<[1, 2]>;   // never
15
16// ---- 2. 多个 infer 与位置对应 ----
17type Swap<T> = T extends [infer A, infer B] ? [B, A] : never;
18type E = Swap<[string, number]>;     // [number, string]
19
20// 三个位置
21type Rotate<T> = T extends [infer A, infer B, infer C] ? [B, C, A] : never;
22type F = Rotate<[1, 2, 3]>;          // [2, 3, 1]
23
24// ---- 3. 同名 infer 多次出现(会推断成"联合")----
25// 实测:SameName<[1, 2]> 的结果是 1 | 2
26type SameName<T> = T extends [infer X, infer X] ? X : never;
27type SameResult = SameName<[1, 2]>;   // 1 | 2
28
29// 用函数参数位置实现"交集":逆变位置会做交叉
30// 这是 UnionToIntersection 的原理
31type UnionToIntersection<U> =
32  (U extends unknown ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
33type G = UnionToIntersection<{ a: 1 } | { b: 2 }>;   // { a: 1 } & { b: 2 }
34
35// ---- 4. 递归 infer ----
36type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T;
37type H = DeepUnwrap<Promise<Promise<string>>>;       // string
38
39// 递归提取嵌套数组元素
40type DeepElement<T> = T extends (infer U)[] ? DeepElement<U> : T;
41type I = DeepElement<string[][]>;                    // string
42
43// ---- 5. infer 提取函数各部分 ----
44type Fn = (a: string, b: number) => boolean;
45
46type Ret<T> = T extends (...args: never[]) => infer R ? R : never;
47type Params<T> = T extends (...args: infer P) => unknown ? P : never;
48type FirstParam<T> = T extends (first: infer F, ...rest: never[]) => unknown ? F : never;
49type ThisOf<T> = T extends (this: infer U, ...args: never[]) => unknown ? U : never;
50
51type J = Ret<Fn>;          // boolean
52type K = Params<Fn>;       // [string, number]
53type L = FirstParam<Fn>;   // string
54
55// ---- 6. infer 提取构造函数 ----
56type InstanceOf<T> = T extends new (...args: never[]) => infer R ? R : never;
57type CtorParams<T> = T extends new (...args: infer P) => unknown ? P : never;
58
59class Point { constructor(public x: number, public y: number) {} }
60type M = InstanceOf<typeof Point>;   // Point
61type N = CtorParams<typeof Point>;   // [number, number]
62
63// ---- 7. infer 提取对象的值类型 ----
64type ValueOf<T> = T extends Record<string, infer V> ? V : never;
65type O = ValueOf<{ a: 1; b: 2 }>;    // 1 | 2(Record<string, V> 匹配)
66
67// ---- 8. infer 在模板字面量里 ----
68type ParseKV<T> = T extends `${infer K}=${infer V}` ? [K, V] : never;
69type P = ParseKV<"name=rainboy">;    // ["name", "rainboy"]
70
71// 提取路径参数
72type PathParam<T> = T extends `${string}:${infer P}` ? P : never;
73type Q = PathParam<"/users/:id">;    // "id"
74
75const a: A = "a";
76const b: B = undefined as never;
77const c: C = "a";
78const d: D = undefined as never;
79const e: E = [1, "a"];
80const f: F = [2, 3, 1];
81const g: G = { a: 1, b: 2 };
82const h: H = "s";
83const i: I = "s";
84const j: J = true;
85const k: K = ["a", 1];
86const l: L = "a";
87const m: M = new Point(1, 2);
88const n: N = [1, 2];
89const o: O = 1;
90const p: P = ["name", "rainboy"];
91const q: Q = "id";
92
93console.log(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q);
1a undefined a undefined [ 1, 'a' ] [ 2, 3, 1 ] { a: 1, b: 2 } s s true [ 'a', 1 ] a Point { x: 1, y: 2 } [ 1, 2 ] 1 [ 'name', 'rainboy' ] id

infer 带约束(TS 4.7+)

1type FirstString<T> = T extends [infer S extends string, ...unknown[]] ? S : never;
2
3type A = FirstString<["a", 1]>;   // "a"
4type B = FirstString<[1, 2]>;     // never(1 不满足 string)

约束不满足时该分支直接不匹配。对比不加约束的写法,需要在结果里再检查一次:

1type FirstStringLoose<T> = T extends [infer S, ...unknown[]]
2  ? S extends string ? S : never
3  : never;

多个 infer 按位置对应

1type Swap<T> = T extends [infer A, infer B] ? [B, A] : never;
2type E = Swap<[string, number]>;     // [number, string]
3
4type Rotate<T> = T extends [infer A, infer B, infer C] ? [B, C, A] : never;
5type F = Rotate<[1, 2, 3]>;          // [2, 3, 1]

同名 infer 产生联合

同一个名字在多个位置出现时,结果是联合:

1type SameName<T> = T extends [infer X, infer X] ? X : never;
2type R = SameName<[1, 2]>;   // 1 | 2(实测确认)

要得到交叉(交集),需要利用函数参数的逆变位置:

1type UnionToIntersection<U> =
2  (U extends unknown ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
3
4type G = UnionToIntersection<{ a: 1 } | { b: 2 }>;   // { a: 1 } & { b: 2 }

这是 UnionToIntersection 的实现原理——参数位置逆变,多个候选类型在逆变位置合并时取交叉。

递归 infer

1type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T;
2type H = DeepUnwrap<Promise<Promise<string>>>;   // string
3
4type DeepElement<T> = T extends (infer U)[] ? DeepElement<U> : T;
5type I = DeepElement<string[][]>;                // string

提取函数的各个部分

1type Fn = (a: string, b: number) => boolean;
2
3type Ret<T> = T extends (...args: never[]) => infer R ? R : never;
4type Params<T> = T extends (...args: infer P) => unknown ? P : never;
5type FirstParam<T> = T extends (first: infer F, ...rest: never[]) => unknown ? F : never;
6type ThisOf<T> = T extends (this: infer U, ...args: never[]) => unknown ? U : never;

注意 (...args: never[]) 的写法——用 never[] 而不是 any[],因为参数位置逆变,never[] 是最宽松的匹配。

提取构造函数

1type InstanceOf<T> = T extends new (...args: never[]) => infer R ? R : never;
2
3class Point { constructor(public x: number, public y: number) {} }
4type M = InstanceOf<typeof Point>;   // Point

typeof Point 是构造函数类型(见 类既是值又是类型),infer R 提取出实例类型。

在模板字面量里 infer

1type ParseKV<T> = T extends `${infer K}=${infer V}` ? [K, V] : never;
2type P = ParseKV<"name=rainboy">;    // ["name", "rainboy"]
3
4type PathParam<T> = T extends `${string}:${infer P}` ? P : never;
5type Q = PathParam<"/users/:id">;    // "id"

详见 模板字面量类型。

速查

需求写法
阻止分发[T] extends [U]
探测 any0 extends 1 & T
探测 never[T] extends [never]
infer 加约束infer S extends string
提取返回类型(...args: never[]) => infer R
联合转交叉利用函数参数逆变位置

相关