第 7 章 · 条件类型与 infer
- 掌握条件类型
T extends U ? X : Y的「类型 if/else」语义 - 理解分布式条件类型:联合类型如何被逐个展开
- 用
infer从类型中提取子类型(返回值、参数、元素……) - 用递归条件类型处理不定长、嵌套的结构
- 手写
Exclude、ReturnType、Parameters、Awaited等工具
7.1 条件类型:类型世界的 if/else
7.1.1 基本语法
type IsString<T> = T extends string ? true : false
type A = IsString<'hello'> // true
type B = IsString<42> // false
type C = IsString<string> // true
T extends U ? X : Y 就是「如果 T 是 U 的子类型,则 X,否则 Y」。它是类型系统里唯一的「分支」机制,也是后面所有类型体操的地基。
7.1.2 条件类型能做什么
典型场景——根据输入类型产出不同输出:
type ToArray<T> = T extends unknown[] ? T[number][] : T
type A = ToArray<number> // number(不是数组,原样返回)
type B = ToArray<[1, 2, 3]> // number[](是数组,取出元素类型再包一层)
7.1.3 [T] 包装:禁止分发
条件类型的 T extends U 中,如果 T 是裸的类型参数,会分发(见 7.2)。想让它「整体判断」而不是分发,用元组包一层:
type IsNever<T> = [T] extends [never] ? true : false
type A = IsNever<never> // true
type B = IsNever<string> // false
[T] extends [never]是判断never的标准写法。直接写T extends never会因为分发返回never而不是true,这是类型体操最常见的坑之一。
7.2 分布式条件类型(重点)
7.2.1 联合被逐个展开
当条件类型左侧是裸类型参数、且实参是联合时,联合的每个成员都会单独走一遍条件:
type Distribute<T> = T extends string ? 'str' : 'other'
type R = Distribute<'a' | 42 | true>
// 分别算:'a' → 'str';42 → 'other';true → 'other'
// 结果:'str' | 'other'
而不是「整个联合一起判断」。这就是分布式条件类型。
T extends U ? X : Y 里 T 是「裸类型参数」时,TS 把 T 的每个联合成员当一次「调用」,各自求值再并成联合。想关闭这个行为,就用 [T] 包起来。
7.2.2 手写 Exclude:从联合里剔除
Exclude<T, U> = 剔除 T 中是 U 的成员。利用分布式,一行搞定:
type MyExclude<T, U> = T extends U ? never : T
type R = MyExclude<'a' | 'b' | 'c', 'a'>
// 'a' → never;'b' → 'b';'c' → 'c'
// 结果:'b' | 'c'(never 自动消失)
'a' 被换成 never,而 never 在联合里会被自动吸收——于是「剔除」就完成了。
7.2.3 手写 If:布尔分支
type MyIf<C extends boolean, T, F> = C extends true ? T : F
type A = MyIf<true, 'yes', 'no'> // 'yes'
type B = MyIf<false, 'yes', 'no'> // 'no'
7.3 infer:从类型里「提取」子类型
infer 在条件类型里声明一个「待推断」的类型变量,让 TS 去拆解:
type GetReturn<T> = T extends (...args: any[]) => infer R ? R : never
type A = GetReturn<() => string> // string
type B = GetReturn<(n: number) => number[]> // number[]
7.3.1 手写 ReturnType
type MyReturnType<T extends (...args: any[]) => any> =
T extends (...args: any[]) => infer R ? R : never
infer R 匹配「函数的返回类型」。约束 T extends (...args: any[]) => any 保证传入的是函数。
7.3.2 手写 Parameters:提取参数元组
type MyParameters<T extends (...args: any[]) => any> =
T extends (...args: infer P) => any ? P : never
type P = MyParameters<(a: string, b: number) => void>
// [string, number]
| 工具 | 提取 | 写法 |
|---|---|---|
ReturnType | 返回值 | (...args) => infer R |
Parameters | 参数元组 | (...args: infer P) => any |
一个是 infer 在返回值位置,一个在参数位置——infer 站在哪,就提取哪。
7.3.3 从数组/元组提取元素
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never
type A = First<[1, 2, 3]> // 1
type B = Last<[1, 2, 3]> // 3
[infer F, ...any[]] 拆出「第一个元素 + 其余」;[...any[], infer L] 拆出「其余 + 最后一个」。
7.4 递归条件类型:处理不定长结构
7.4.1 数组递归
从 Promise 里逐层解包,就是递归:
type MyAwaited<T extends PromiseLike<any>> =
T extends PromiseLike<infer U>
? U extends PromiseLike<any>
? MyAwaited<U> // 还是 Promise,继续解
: U // 不是 Promise 了,取出
: never
type A = MyAwaited<Promise<string>> // string
type B = MyAwaited<Promise<Promise<number>>> // number
type C = MyAwaited<Promise<Promise<Promise<boolean>>>> // boolean
7.4.2 递归的「形状」与终止条件
每个递归条件类型都有同一副骨架:
type Recursive<T> =
T extends SomePattern<infer Rest>
? Combine<Recursive<Rest>> // 拆一步 → 递归 → 组合
: BaseCase // 拆不动了 → 终止
- 终止条件:
infer拆不动时的 else 分支 - 递归步:把拆出的子结构喂回自己
7.4.3 递归深度:TS 的限制
递归类型有实例化深度上限(TS 6 里大约几千层)。深度用不完没关系,但你要意识到:无限递归会报错,所以终止条件必须存在。
本章小结
- 条件类型
T extends U ? X : Y是类型世界的 if/else - 裸类型参数的分布式:联合被逐个展开,
Exclude因此一行实现 infer站在哪就提取哪:返回值、参数、数组元素……- 递归条件类型用「拆一步 → 递归 → 组合 + 终止」处理不定长结构
- 本章的 6 道挑战题全是这些手法的直接应用
本章练习
挑战题
| 题号 | 题目 | 难度 | 考察点 |
|---|---|---|---|
| 00043 | 实现 Exclude | easy | 分布式 + never 吸收 |
| 00268 | If | easy | 条件类型分支 |
| 00002 | 获取函数返回类型 | medium | infer 返回值 |
| 03312 | Parameters | easy | infer 参数元组 |
| 00189 | Awaited | easy | 递归解包 Promise |
| 00010 | 元组转合集 | medium | T[number] |
这 6 题的解法在 7.2 / 7.3 / 7.4 里几乎逐字对应。做完它们,你就掌握了「类型层面做分支与提取」的基本功。
代码练习
-
分布式验证:写
type Collect<T> = T extends any ? [T] : never,分别传'a' | 'b'和string[],观察结果差异,体会「裸类型参数分发」与「包装类型不分发」。 -
手写 ReturnType / Parameters / First / Last:不看本章代码,各手写一遍,并给一个带默认参数的函数验证
Parameters能拿到带默认值的参数类型。 -
递归 Flatten:写
type Flatten<T extends any[]>把嵌套数组打平([[1, [2]], 3]→[1, 2, 3])。提示:元素是数组就递归展开,不是数组就保留。 -
递归解包:写
DeepUnwrap<T>,把任意层Promise解开(等价于MyAwaited),并验证对Promise<Promise<Promise<number>>>得number。 -
is 判定 + 条件类型结合:写
type IsTuple<T>(第 14 章会用到):判断T是不是元组——先判never,再判是否readonly any[],再看number extends T['length']是否为假。