跳到主要内容

第 7 章 · 条件类型与 infer

读完本章你会
  • 掌握条件类型 T extends U ? X : Y 的「类型 if/else」语义
  • 理解分布式条件类型:联合类型如何被逐个展开
  • infer 从类型中提取子类型(返回值、参数、元素……)
  • 用递归条件类型处理不定长、嵌套的结构
  • 手写 ExcludeReturnTypeParametersAwaited 等工具

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 : YT 是「裸类型参数」时,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 与 Parameters
工具提取写法
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实现 Excludeeasy分布式 + never 吸收
00268Ifeasy条件类型分支
00002获取函数返回类型mediuminfer 返回值
03312Parameterseasyinfer 参数元组
00189Awaitedeasy递归解包 Promise
00010元组转合集mediumT[number]

这 6 题的解法在 7.2 / 7.3 / 7.4 里几乎逐字对应。做完它们,你就掌握了「类型层面做分支与提取」的基本功。

代码练习

  1. 分布式验证:写 type Collect<T> = T extends any ? [T] : never,分别传 'a' | 'b'string[],观察结果差异,体会「裸类型参数分发」与「包装类型不分发」。

  2. 手写 ReturnType / Parameters / First / Last:不看本章代码,各手写一遍,并给一个带默认参数的函数验证 Parameters 能拿到带默认值的参数类型。

  3. 递归 Flatten:写 type Flatten<T extends any[]> 把嵌套数组打平([[1, [2]], 3][1, 2, 3])。提示:元素是数组就递归展开,不是数组就保留。

  4. 递归解包:写 DeepUnwrap<T>,把任意层 Promise 解开(等价于 MyAwaited),并验证对 Promise<Promise<Promise<number>>>number

  5. is 判定 + 条件类型结合:写 type IsTuple<T>(第 14 章会用到):判断 T 是不是元组——先判 never,再判是否 readonly any[],再看 number extends T['length'] 是否为假。