写类型体操的时候,infer关键字往往是整个工具类型的灵魂。可一旦这个待推断类型被埋进两层甚至三层条件类型里,很多人就会发现推断结果开始变得不可控:有时推断出的是联合类型,有时干脆推断失败退回unknown。要理解这些现象,必须先弄清楚TypeScript在处理嵌套条件类型时,推断候选是如何被收集、合并以及裁决的。

一、infer的匹配本质:从结构展开到推断候选收集
很多开发者把infer T理解为声明一个类型变量,这种理解在简单场景下没问题,但在嵌套场景里会踩坑。准确地说,infer T标记了一个推断位置,TypeScript在检查待匹配类型与模板类型的结构一致性时,会把模板中infer T所在位置对应的实际类型记录为推断候选。
关键点在于:候选的收集是逐层进行的。外层条件类型的infer会先确定一个粗粒度的类型范围,内层条件类型再基于这个范围继续细分。如果内层的infer位置依赖于外层的推断结果,而外层推断失败或产生了多个候选,内层的推断就会随之出现意外行为。看一个典型例子:
type Flatten<T> = T extends Array<infer Inner>
? Inner extends Promise<infer V>
? V
: Inner
: T;
// 数组的元素类型是 Promise<number>,两层推断都能命中
type A = Flatten<Promise<number>[]>; // number
// 元素本身不是数组包装,外层条件直接走 false 分支
type B = Flatten<string>; // string这个例子能正常工作,是因为两层infer各自的结构匹配是独立的。但下面这种写法就有问题了:
type BadExtract<T> = T extends (arg: infer P) => infer R
? P extends string
? R extends Promise<infer V>
? V
: never
: never
: never;
type Fn = (arg: number) => Promise<string>;
// P 被推断为 number,不是 string,走 never 分支
type R1 = BadExtract<Fn>; // never问题不在嵌套本身,而在于内层条件对外层推断结果做了过滤。理解了这一点,排查推断失败的第一步永远是:先手动展开每一层的推断结果,确认是哪一层把类型引向了错误的分支。
二、推断位置的可变性:协变与逆变如何影响嵌套推断结果
当同一个infer T在嵌套条件类型的多个位置出现时,TypeScript会根据位置的可变性决定合并策略。这一规则在嵌套场景中尤其重要,因为嵌套往往意味着同一个推断变量被引用多次。
具体规则是:出现在协变位置(比如函数返回值、对象属性、数组元素)的多个候选会被合并为联合类型;出现在逆变位置(函数参数)的多个候选会被合并为交叉类型;如果一个变量同时出现在协变和逆变位置,且存在多个候选,推断就会失败,退回unknown或直接报错。用一个经典例子说明:
type Covariant<T> = T extends {
a: (x: string) => infer R,
b: (x: number) => infer R,
} ? R : never;
// R 出现两个协变候选,合并为联合类型
type C1 = Covariant<{ a: () => string, b: () => number }>; // string | number
type Contravariant<T> = T extends {
a: (x: infer P) => void,
b: (y: infer P) => void,
} ? P : never;
// P 出现两个逆变候选,合并为交叉类型
type C2 = Contravariant<{ a: (x: string) => void, b: (x: number) => void }>;
// string & number,实际上收敛为 never在多重嵌套条件下,这个规则会叠加生效。比如外层条件推断出的类型被传入内层条件,内层又在函数参数位置引用了新的infer,此时内层的候选合并发生在内层求解阶段,与外层互不干扰。但如果两层条件共享同一个类型参数(这在递归类型中很常见),可变性问题就会跨层传递,这也是许多递归工具类型在中途退化成unknown的根本原因。
实践中应对的办法是:给每一层的推断变量起不同的名字,避免跨层复用;当确实需要联合多个候选时,显式地用union的方式收集,而不是依赖可变性的隐式合并。
三、推断失败的兜底与嵌套条件的正确拆分姿势
推断失败的常见表现有两种:一是infer位置没有匹配到任何类型,变量被固定为unknown;二是匹配到了多个无法合并的候选。TypeScript允许给infer加约束和默认值,这在嵌套场景中是重要的防御手段:
type SafeAwaited<T> = T extends Promise<infer V>
? SafeAwaited<V>
: T extends { then: (onfulfilled: infer F) => any }
? F extends (value: infer V, ...rest: any) => any
? SafeAwaited<V>
: never
: T;
type S1 = SafeAwaited<Promise<Promise<number>>>; // number
type S2 = SafeAwaited<string>; // string上面这个仿照内置Awaited的简化实现展示了嵌套条件的推荐结构:每一层只负责一个明确的判断维度,层与层之间通过递归调用传递中间结果,而不是在一层里塞进多个infer。这样做的好处是编译器每一步的求解范围都很小,出错时也容易定位。
另一个实用技巧是先降维再推断。当待推断类型藏在深层结构里时,可以先用一层条件类型把外层结构剥掉,得到一个扁平的中间类型,再在第二层里做infer。比如从嵌套对象中提取叶子类型,先递归拍平再统一推断,比一次性在深层位置写infer稳定得多。同时,对推断结果加上约束(如infer V extends object)可以避免unknown在后续层级中扩散,让报错信息更精确。掌握这些拆分与兜底策略后,多层嵌套条件类型的推断就不再依赖运气,而是可以推演的确定过程。
TypeScript条件类型类型推断修改时间:2026-09-16 04:54:35