TypeScript的条件类型配合infer关键字,可以实现类似模式匹配的类型提取能力。单个infer的用法大多数人都能掌握,但一旦待推断类型被放进多重嵌套的条件类型中,推断结果就变得难以预测。同样的写法,有时得到期望的精确类型,有时却得到never或者unknown。这篇文章围绕嵌套条件类型中infer的推断规则展开,分析协变与逆变位置对推断结果的影响,并给出几个典型的类型体操实例。

条件类型的匹配机制与infer的基本原理
要理解嵌套场景下的推断行为,首先要知道条件类型的匹配规则。条件类型的语法是T extends U ? X : Y,编译器会先检查T是否可以分配给U。当U中出现了infer R这样的占位符时,编译器会尝试把T的结构与U进行匹配,一旦匹配成功,就把T中对应位置的具体类型赋给R。这个过程本质上是一种结构化的模式匹配。
匹配的前提是extends两边的结构能够对齐。比如T extends Promise<infer R> ? R : T这个经典写法,当T确实是Promise结构时,R就被推断为Promise内部的包装类型;否则走false分支返回T本身。这里的关键在于:infer所在的位置必须和T中对应位置的结构严格对齐,多一层或少一层包装都会导致匹配失败。
还有一个容易被忽略的点:当传入的类型参数是联合类型时,条件类型默认是分布式的。也就是说(A | B) extends Promise<infer R>会被拆成A extends Promise<infer R>和B extends Promise<infer R>分别判断,最后把各分支的结果重新组成联合类型。这个特性在嵌套条件类型中会逐层生效,是很多意外结果的来源。
多重嵌套时infer的推断顺序与协变逆变规则
当infer出现在函数参数、返回值等多个位置时,推断结果取决于该位置的型变性质。官方规则可以概括为:多个同位置的候选类型如果处于协变位置(比如返回值),最终合成联合类型;处于逆变位置(比如函数参数),最终合成交叉类型。如果候选类型同时出现在协变和逆变位置,则直接推断为never。理解这条规则,是写对嵌套infer的基础。
来看一个从对象属性中提取类型的例子。假设有一个嵌套的条件类型,想提取所有函数的返回值:
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Obj = {
a: () => string;
b: () => number;
};
// 借助映射类型逐个属性处理,infer在每一层条件类型中独立推断
type AllReturns<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => infer R ? R : never
}[keyof T];
type Result = AllReturns<Obj>; // string | number
这里infer虽然只写了一次,但通过映射类型被应用到了每个属性上,每个属性独立走一遍条件判断。由于返回值位置是协变的,多个候选string和number最终合成为联合类型string | number。如果把infer放到参数位置,结果就会变成交叉类型,比如string & number,而这个交叉类型实际上是never,因为没有任何类型能同时是string和number。
嵌套还带来一个顺序问题。多重条件类型是从外向内逐层求值的,外层的条件判断结果决定了内层拿到的类型。如果外层判断已经把类型缩小,内层的infer就在缩小后的类型上匹配。写嵌套时建议把每一层的职责想清楚:外层负责剥壳,内层负责提取,不要让两层infer抢同一个位置。
递归嵌套提取深层Promise的实战
异步代码经过async修饰后,返回值会被Promise层层包裹。想拿到最里层的类型,就需要递归条件类型配合infer。这是嵌套推断最经典的应用场景:
type Awaited<T> = T extends PromiseLike<infer V>
? Awaited<V> // 匹配成功就继续剥下一层
: T; // 不再是Promise时返回自身
async function fetchUser() {
return fetchUser();
}
type Deep = Awaited<ReturnType<typeof fetchUser>>; // 递归到底的具体类型
这段代码的关键在于递归调用自身时,V已经比T少了一层Promise包装。TypeScript从4.1版本开始支持递归条件类型,但要求递归必须最终能够终止,也就是某一层要能走到false分支。如果模式写得不对,比如模式本身每次匹配都不消耗包装层,就会报类型实例化过深的错误。
类似思路还可以用来递归扁平化数组。利用分布条件类型把联合类型的数组逐个拆开:
type Flatten<T> = T extends Array<infer Inner> ? Flatten<Inner> : T; type Nested = Array<Array<Array<number>>>; type Flat = Flatten<Nested>; // number
注意这里的技巧是模式写成Array<infer Inner>而不是infer Inner[],前者兼容元组和readonly数组,适用范围更广。当传入的是数组的联合类型时,分布行为会把每个成员分别递归处理,最后合并成扁平的联合类型,这正是我们想要的语义。
常见误区与调试手段
第一个常见误区是忘记阻止分布行为。如果希望对联合类型整体判断而不是逐个成员判断,需要用方括号包裹:[T] extends [U]。比如判断T是否恰好是boolean类型,直接写T extends boolean ? ...会把true和false拆开分别处理,用元组包裹才能拿到整体视角。
第二个误区是在同一层的多个位置使用同名infer变量,导致协变逆变冲突推断出never。解决办法是给不同位置的infer起不同的名字,再在外层分别引用,或者用条件类型逐个提取。
调试嵌套条件类型时,最实用的办法是借助IDE的类型悬停提示,从最内层开始逐层验证。也可以定义临时类型别名,把中间结果打印出来。另一个工具是官方发行说明中的可分配性检查技巧:用一个故意的赋值错误,让编译器在错误信息中展示它推断出的具体类型:
type Debug<T> = T extends Promise<infer R> ? R : never; type Check = Debug<Promise<Promise<string>>>; // 用赋值错误触发提示,观察编译器给出的类型 declare const check: Check; const target: string = check; // 报错信息会展示真实推断结果
总的来说,多重嵌套条件类型中的infer推断遵循明确规则:外层到内层逐层求值、分布行为逐层传播、协变合成联合、逆变合成交叉、冲突则never。把这几条规则记牢,再配合递归模式处理深层结构,大部分复杂的类型体操问题都能迎刃而解。遇到不符合预期的结果时,先检查分布行为是否被触发,再检查infer是否同时出现在协变和逆变位置,通常就能定位到问题所在。
TypeScript条件类型infer类型推断修改时间:2026-09-04 11:04:49