条件类型是类型层面的 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] |
探测 any | 0 extends 1 & T |
探测 never | [T] extends [never] |
infer 加约束 | infer S extends string |
| 提取返回类型 | (...args: never[]) => infer R |
| 联合转交叉 | 利用函数参数逆变位置 |