TypeScript的条件类型配合infer关键字,是实现复杂类型变换的利器。不过当待推断类型不是简单地位于extends右侧的顶层,而是嵌套在另一个条件类型的判断位置时,很多写法会得到与直觉不符的结果。这篇文章就来拆解这个机制,看看编译器在遇到嵌套条件类型时的处理顺序,以及怎样组织类型代码才能让推断按预期进行。

先回顾infer的基本匹配规则
infer只能出现在条件类型的extends子句中,它声明一个待推断的类型变量,编译器会用待检查的类型去匹配extends右侧的模式,匹配成功后把捕获到的类型绑定到这个变量上。最经典的例子是从数组中提取元素类型:
type ElementType<T> = T extends (infer E)[] ? E : never; type A = ElementType<string[]>; // string type B = ElementType<number[][]>; // number[]
这里的关键在于,extends右侧是一个明确的模式,编译器把这个模式当作模板去解构T。推断只在模式匹配这一步发生,一旦进入真分支或假分支,推断已经结束,infer变量在分支内就是一个确定的类型。
还有一条容易被忽略的规则:当推断点出现在函数参数(逆变位置)时,若匹配过程中存在多个候选,TypeScript会取它们的交叉类型;而出现在返回值或数组元素这类协变位置时,会取联合类型。这条规则在嵌套条件类型里会反复出现,是理解后面现象的基础。
嵌套条件类型中的推断为什么会失真
问题场景通常是下面这样的:我们希望先推断出某个内部结构,再根据这个结构做二次判断。于是很自然地写出嵌套的条件类型,把infer放进了内层的extends左侧或右侧的复杂位置。
type Flatten<T> = T extends object
? T extends { value: infer V }
? V extends Promise<infer R>
? R
: V
: never
: T;
type X = Flatten<{ value: Promise<string> }>; // string,符合预期
type Y = Flatten<{ value: number }[]>; // never,为什么?第一个用例正常,第二个却得到never。原因在于条件类型的分布性:当泛型参数T是裸类型参数出现在extends左侧时,条件类型会 distributes over union,也就是对联合类型的每个成员分别求值再合并。对于{ value: number }[],外层判断它是否是object,数组确实是object,于是进入真分支;但内层的{ value: infer V }模式匹配失败,因为数组上没有value属性,走到了never分支。
再看一种更隐蔽的情况:待推断类型位于嵌套条件类型的判断位置。也就是说,内层条件类型的extends左侧直接使用了外层推断出来的变量:
type Unwrap<T> = T extends { value: infer V }
? V extends Promise<infer R>
? R
: V
: never;
type Z = Unwrap<{ value: string } | { value: Promise<number> }>;这里外层的推断在分布式求值中各自成功,分别得到string和Promise<number>,随后内层再各自判断,最终结果是string | number。这其实是符合预期的。但如果把V本身放到一个更深的结构里参与匹配,例如[V] extends [infer R] ? R : never,在部分旧版本编译器中会遇到推断推迟的问题——因为V此时还不是具体类型,编译器会把内层条件类型标记为deferred,只有在实例化时才真正求值。理解这一点很重要:嵌套条件类型的求值是自外向内、逐层实例化的,内层使用的推断变量在外层匹配完成后就已经确定,不会再次参与推断。
让嵌套推断正常工作的几种写法
第一种思路是把推断点集中到一处,用函数参数做推断锚点。函数参数是逆变位置,多个候选会取交叉类型,通过精心设计参数结构,可以在一次匹配中同时捕获多个类型变量,避免多层条件类型互相纠缠:
type InferDeep<T> = T extends (
arg: { value: Promise<infer R> }
) => void
? R
: T extends { value: infer V }
? V
: never;
type M = InferDeep<{ value: Promise<boolean> }>; // boolean
type N = InferDeep<{ value: Date }>; // Date这种写法的好处是优先尝试更具体的模式,失败后再退回宽松模式,模式之间的优先级由外层条件类型的分支顺序决定,逻辑清晰且不依赖嵌套的推断变量。
第二种思路是拆分条件类型,让每一层只做一件事。与其写一个多层的巨型条件类型,不如定义几个小的工具类型,逐级传递结果。每一步的推断都发生在顶层extends中,可读性和调试体验都会好很多:
type GetValueType<T> = T extends { value: infer V } ? V : never;
type Awaited<T> = T extends PromiseLike<infer R> ? R : T;
type Resolve<T> = Awaited<GetValueType<T>>;
type P = Resolve<{ value: Promise<Map<string, number>> }>; // Map<string, number>第三种思路适用于字符串场景:利用模板字符串类型的推断点。当待推断的内容位于模板字符串内部时,infer同样可以捕获片段,这在解析路由参数、拆分联合类型的标签前缀时特别有用:
type ParseRoute<S extends string> =
S extends `/${string}/${infer Id}`
? Id extends `${infer N extends number}`
? N
: Id
: never;
type Q = ParseRoute<"/user/42">; // 42(number类型,而非字符串)这里的内层infer N extends number用到了推断约束语法,允许在推断的同时对结果施加约束,等价于先推断再校验,但省去了一层条件判断。
排查嵌套推断问题的实用技巧
遇到推断结果不符合预期时,推荐的做法是逐层剥开验证:把嵌套条件类型的每一层单独提取成临时类型别名,用具体类型实例化后查看结果。借助编辑器的悬停提示或者TypeScript Playground,可以快速定位是哪一层的匹配出了问题。
另外要留意分布式条件类型的触发条件:只有当extends左侧是裸类型参数时才会分布。如果不希望分布,可以用元组包裹一层,写成[T] extends [U] ? X : Y的形式,这样联合类型会作为整体参与匹配,很多诡异的never结果其实都源于不期望的分布式求值。
最后,协变取联合、逆变取交叉的规则在调试时非常关键。如果推断结果出现了一个意料之外的交叉类型,几乎可以断定推断点落在了逆变位置且有多个候选;此时调整推断点的位置,或者改用顶层模式匹配,通常就能解决问题。掌握这几条原则,嵌套条件类型的推断行为就基本可以完全掌控了。
TypeScript条件类型infer类型推断修改时间:2026-09-09 13:55:12