导读:本期聚焦于美园和花创作的《TypeScript条件类型中infer待推断类型处于协变位置时如何推断出联合类型?》,敬请观看详情。infer关键字让TypeScript的条件类型具备了从已有类型中提取结构信息的能力,但很多人发现同样的infer写法,有时得到的是单一类型,有时却得到一个联合类型。这背后的核心原因在于待推断类型所处的可变性位置:当infer R出现在协变位置时,TypeScript会收集所有可能的候选类型并合成联合类型;出现在逆变位置时则合成交叉类型。本文从协变与逆变的基本概念讲起,结合函数返回值、数组元素、Promise等典型协变场景,分析推断规则如何生效,并通过提取函数返回值类型、获取Promise深层resolve类型等实例演示具体用法,最后对比协变与逆变位置推断结果的差异,帮助你写出更精确、更可控的条件类型工具。

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

TypeScript条件类型中infer待推断类型处于协变位置时如何推断出联合类型?

协变与逆变到底是什么

要理解协变位置上的推断规则,得先弄清楚协变本身。简单说,如果类型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 | boolean

infer 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

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0910/53819.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。