TypeScript的infer关键字是条件类型体系里最灵活的能力之一,它允许我们在extends子句中声明一个待推断的变量,然后从匹配的类型中提取出想要的部分。不过写过一阵子之后,你大概率会遇到一个有趣的现象:同样是infer,有时候推断出来的是一个具体的单一类型,有时候却冒出来一个联合类型。比如从一个对象类型里提取所有属性的类型,结果不是某一个属性的类型,而是所有属性类型组成的联合。这个行为并不是Bug,而是由待推断类型所处的可变性位置决定的。当一个infer出现在协变位置时,TypeScript会把所有候选类型收集起来,合成一个联合类型;当它出现在逆变位置时,则会合成交叉类型。本文重点聊聊协变位置上的推断行为。

协变与逆变到底是什么
要理解协变位置上的推断规则,得先弄清楚协变本身。简单说,如果类型A可以赋值给类型B时,由A构造出来的复杂类型F<A>也能赋值给F<B>,那么F就是协变的。最典型的例子是函数返回值:假设有一个返回Dog的函数,它完全可以当作返回Animal的函数来用,因为调用方拿到Dog之后按Animal处理没有任何问题。所以返回值位置是协变的。
反过来,函数参数位置是逆变的:一个接受Animal参数的函数,可以被当作接受Dog的函数使用,方向正好相反。除了函数,数组、Promise、只读属性这些位置也都是协变的,比如Promise<Dog>可以赋值给Promise<Animal>。理解了这些位置分类,才能预判infer的推断结果长什么样。
还有一个关键点:当同一个infer变量在协变位置出现了多次,TypeScript不会报错说推断有歧义,而是把每个位置推断出的候选类型全部收集起来,用联合类型(union)合成。这正是很多工具类型的实现基础。而如果在逆变位置出现多次,合成方式就变成了交叉类型(intersection)。这套规则是TypeScript编译器内建的行为,不需要任何额外配置。
协变位置推断出联合类型的典型场景
最常见的协变位置就是函数返回值。看下面这个经典工具类型的简化版:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function foo(): string { return "hello"; }
function bar(): number { return 42; }
type A = ReturnType<typeof foo>; // string
type B = ReturnType<typeof bar>; // number这里infer R出现在返回值位置,也就是协变位置,每个具体函数只会推断出唯一一个候选类型,所以结果就是那个单一类型。但当传入的是函数类型的联合时,情况就变了:
type F = (() => string) | (() => number); type R = ReturnType<F>; // string | number
联合类型会分发到条件类型中逐个匹配,每次匹配产生一个候选类型,最后合成的结果就是string | number。这个行为体现了协变位置收集候选、合成联合的核心规则。
更有意思的是同一个infer在多个协变位置出现的场景。比如提取对象所有属性值类型的工具:
type ValueOf<T> = T extends { [K in keyof T]: infer V } ? V : never;
type Obj = {
name: string;
age: number;
active: boolean;
};
type Values = ValueOf<Obj>; // string | number | booleaninfer V在映射类型的属性值位置出现了三次,属性值位置是协变的,所以三个候选类型string、number、boolean被收集起来,最终推断出联合类型string | number | boolean。如果没有这条规则,这种提取根本无法实现,因为编译器必须为V选一个确定的类型,而三个属性类型各不相同。
利用协变位置推断的实战技巧
理解了规则之后,可以拿来做很多实用的类型体操。一个常见需求是递归提取Promise最终resolve出来的类型:
type Awaited<T> = T extends PromiseLike<infer V> ? Awaited<V> : T; type P = Promise<Promise<string>>; type Result = Awaited<P>; // string
Promise的类型参数是协变位置,所以infer V能顺利拿到内层类型,配合递归就能剥开任意嵌套层。TypeScript内置的Awaited<T>就是基于类似思路实现的,只是处理了更多边界情况。
再比如获取元组或者数组所有元素的联合类型:
type ElementOf<T> = T extends readonly (infer E)[] ? E : never; type Tuple = [string, number, boolean]; type Elements = ElementOf<Tuple>; // string | number | boolean
数组元素位置同样是协变的。当元组的三个元素类型不同时,同一个infer E对应多个候选,协变规则把它们合并成一个联合。如果这里换成逆变位置,比如把E放到函数参数里,结果就完全不同了。
最后对比一下逆变位置的行为,加深理解:
type Param<T> = T extends (x: infer P) => void ? P : never;
type Fn = (x: string) => void;
type P1 = Param<Fn>; // string
// 逆变位置出现多次时合成交叉类型
type Obj2 = {
a: (x: string) => void;
b: (x: number) => void;
};
type Both = Obj2 extends {
a: (x: infer P) => void;
b: (x: infer P) => void;
} ? P : never;
// Both 的类型是 string & number,即 never可以看到,infer出现在参数位置(逆变位置)时,多个候选会被合成交叉类型,string & number直接坍缩成never。这就是同一个infer在不同可变性位置产生截然不同结果的最好证明。写工具类型时,务必想清楚你要提取的目标处于什么位置:想要联合就用协变位置(返回值、属性值、数组元素),想要交叉或者精确匹配就用逆变位置。掌握这条规则,条件类型的推断行为就完全在你掌控之中了。
TypeScript条件类型类型推断修改时间:2026-09-10 04:48:27