TypeScript的条件类型配合infer关键字,可以实现很多过去只能靠声明重载才能完成的类型运算。不过一旦待推断的变量不是出现在结构的顶层,而是嵌套在函数参数、数组元素或者对象属性的深处,推断行为就会变得微妙起来。同一个infer写在不同的位置,可能得到单一类型、联合类型,也可能是交叉类型,理解这背后的规则是写好复杂泛型工具类型的基础。

先弄清楚infer的基本推断位置规则
在讨论深层嵌套之前,需要先回顾官方文档中一条容易被忽略的规则:当待推断的类型变量出现在多个候选位置时,TypeScript会根据位置决定最终结果。出现在协变位置的多个候选会被推断为联合类型,而出现在逆变位置的多个候选会被推断为交叉类型。
所谓协变位置,最典型的例子是函数返回值和数组元素;逆变位置则是函数的参数。这条规则源于类型系统对类型安全的要求:返回值只被生产,取所有可能类型的联合是安全的;参数只被消费,必须能接受所有调用形态,所以需要交叉。看下面这个经典对比:
type Bar<T> = T extends { x: infer R } ? R : never;
// 协变位置:多个候选推断为联合类型
type Foo = Bar<{ x: string } | { x: number }>; // string | number
// 逆变位置:多个候选推断为交叉类型
type Fn = (x: string) => void | ((x: number) => void);
type Params = Parameters<string extends never ? never : Fn>;这个规则看似简单,但它是理解深层嵌套推断的钥匙。当infer藏在多层结构中时,TypeScript会沿着结构的可赋值性检查路径收集候选类型,最终再依据这些候选所在的位置是协变还是逆变形来归并结果。
深层嵌套的典型场景与处理方式
场景一:提取深层Promise链的最终值
递归条件类型是穿透深层嵌套的利器。以获取Promise嵌套链条最内层类型为例,很多初学者会写出一次性匹配的写法,然后发现推断失败。正确的做法是递归展开:
type Awaited<T> = T extends PromiseLike<infer V> ? Awaited<V> // 递归剥掉一层 : T; type R1 = Awaited<Promise<Promise<Promise<string>>>>; // string
这里的关键在于每一步只推断一层Promise的内部类型,然后用推断结果继续递归。如果把infer写在多层泛型内部,试图一次性挖到底,TypeScript的推断器会因为无法建立候选与位置的对应关系而放弃,退化成unknown或者直接报错。
场景二:解析嵌套函数的参数与返回值
高阶函数场景下,infer经常需要同时出现在逆变位置(参数)和协变位置(返回值)。比如提取一个多层柯里化函数最底层的参数:
type DeepestParams<T> =
T extends (...args: infer A) => infer R
? R extends (...args: any[]) => any
? DeepestParams<R> // 返回值还是函数,继续下探
: A // 到底了,返回最深层参数
: never;
type F = (a: string) => (b: number) => (c: boolean) => void;
type P = DeepestParams<F>; // [c: boolean]注意这个例子里参数用了infer A,它处于逆变位置。如果函数类型本身是联合类型,比如两个函数签名的联合,那么A会被推断为两个参数列表对应位置的交叉类型,这往往不是我们想要的。此时需要先用分布式条件类型把联合拆开,再逐个提取后重新组合。
场景三:树形结构中的深层属性提取
处理树形数据时,infer配合递归可以提取所有叶子节点的类型:
type Leaf<T> =
T extends { children: infer C }
? C extends readonly unknown[]
? Leaf<C[number]> // 下探到每个子节点
: Leaf<C>
: T;
interface NodeA { value: string; children: NodeB[] }
interface NodeB { value: number; children: NodeC[] }
interface NodeC { value: boolean }
type L = Leaf<NodeA>; // string | number | boolean由于数组元素是协变位置,多个候选最终归并为联合类型,恰好符合“收集所有叶子”的语义。这里要注意C[number]这个索引访问,它是从数组类型转向元素类型的桥梁,如果直接对数组类型递归,会导致匹配不到对象结构而提前终止。
推断失败时的排查思路
当深层嵌套的infer没有得到预期结果时,可以按以下顺序排查。第一,确认extends两边的结构形状是否真正对齐,包括可选属性、readonly修饰符和数组与元组的差异,形状不一致时infer会直接走false分支。第二,检查是否碰到了推断候选的归并陷阱,如果输入可能是联合类型,条件类型会自动分发,infer在每个分支里只看到单个成员,这既是坑也是解法。第三,注意同构性限制,TypeScript要求推断时结构的骨架一致,骨架里不能有无法确定的泛型穿插在infer周围,否则推断器会拒绝猜测。
另一个实用技巧是把深层嵌套拆成多个具名的中间条件类型,每一步只处理一层,用类型别名串联起来。这不仅能显著降低单步推断的复杂度,还能在每一步通过悬浮提示观察中间结果,定位到底是哪一层开始偏离预期。相比之下,把所有逻辑压缩进一个巨大的嵌套三元表达式,调试起来要痛苦得多。
最后提醒一点,TypeScript对递归条件类型的实例化深度有限制,过深的嵌套结构可能触发类型实例化过深的报错。如果数据结构层级不可控,可以考虑在某一层设置终止哨兵,或者放宽该层的类型约束,用可读性和精确度换取编译的稳定性。掌握协变逆变位置这条主线,再辅以递归拆层的手段,绝大多数深层嵌套的infer问题都能迎刃而解。
TypeScript条件类型infer类型推断修改时间:2026-09-08 00:26:35