TypeScript的条件类型(Conditional Types)是类型系统中表达能力最强的特性之一,而infer关键字则是让条件类型从“判断”升级为“提取”的关键。不过不少人在写条件类型时会遇到一个困惑:为什么待推断类型必须出现在extends左侧的检查类型中,而推断出的结果却要到true分支里才能拿到?如果把infer写到extends右侧或者false分支,编译器会直接抛出错误提示infer declarations are only permitted in the extends clause of a conditional type。理解这条位置规则,是掌握条件类型进阶用法绕不开的一关。

一、条件类型的求值机制决定了infer的位置
要理解infer的位置限制,先要弄清条件类型是如何求值的。一个条件类型的完整形式是T extends U ? X : Y,其中T是待检查类型,U是extends右侧的匹配模式。TypeScript在求值时会将T与U做结构匹配,如果匹配成功就采用X分支的类型,否则采用Y分支的类型。
这个匹配过程本质上是一个“模式解构”的过程,类似于一层类型层面的正则匹配。extends右侧的U充当匹配模板,左侧的T是被匹配的目标。当U中出现infer R这样的待推断占位符时,TypeScript会在匹配过程中从T里反向计算出R应该是什么类型。也就是说,推断动作发生在“T与U做匹配”的那一瞬间,结果则被记录到R这个名字上,供后续分支使用。
type MyReturnType<T> = T extends (...args: never[]) => infer R ? R : never; // 匹配过程:把 (...args: any[]) => string 与模板对齐 // R 被推断为 string type A = MyReturnType<() => string>; // string type B = MyReturnType<() => number>; // number
从这个例子可以看出,infer R出现在extends右侧的匹配模板中,而推断结果R在true分支被引用。这不是随意的语法设计:如果允许在false分支引用R,那么当匹配失败时R根本没有任何值可以绑定,分支内的引用就成了悬空变量。所以TypeScript直接从语法层面禁止了这种写法,避免产生无法求值的类型表达式。
换句话说,infer必须出现在条件类型的“条件位置”,也就是extends子句的匹配模板里,而使用它的位置只能是true分支。这种“推断发生在条件中、消费发生在结果里”的约束,和JavaScript中解构赋值的思维有些类似——先从结构中提取值,再在后续代码中使用这个值。
二、内置工具类型中的典型用法拆解
TypeScript标准库里的许多工具类型都是这套规则的经典范例。最常用的几个包括ReturnType、Parameters、ConstructorParameters和Awaited,它们的实现全部依赖infer在匹配模板中的正确放置。
先看Parameters,它的作用是提取函数的参数元组类型。实现思路是把函数类型与(...args: infer P) => any这个模板做匹配,参数列表整体被标记为待推断的P:
type MyParameters<T extends (...args: any[]) => any> = T extends (...args: infer P) => any ? P : never; type Fn = (name: string, age: number) => boolean; type P = MyParameters<Fn>; // [name: string, age: number]
再看Awaited,它负责递归解包Promise,从中提取最终 resolved 的值类型。这里infer放在了Promise泛型参数的位置,同时true分支里递归调用自身,实现对多层Promise的展开:
type MyAwaited<T> = T extends PromiseLike<infer V> ? MyAwaited<V> : T; type R1 = MyAwaited<Promise<Promise<string>>>; // string type R2 = MyAwaited<number>; // number
这些例子有一个共同点:infer总是出现在匹配模板的结构化位置上——函数返回值位置、参数位置、Promise的泛型参数位置、数组元素位置等等。位置不同,提取的内容就不同。比如想提取数组的最后一个元素,可以把infer放在元组的最后一个槽位:
type Last<T extends any[]> = T extends [...rest: any[], infer L] ? L : never; type L1 = Last<[string, number, boolean]>; // boolean
掌握这种“模板槽位思维”之后,大部分类型体操题目都会变得有章可循:你只需要想清楚目标类型长什么样,再把infer放到需要挖出来的那个位置即可。
三、分布式的陷阱与进阶应用
条件类型有一个容易被忽视的特性:当检查类型是裸类型参数时,条件类型会自动分布式地应用到联合类型的每一个成员上。这会导致infer的推断结果有时和我们直觉不符。例如直接提取联合类型函数的返回值时:
type Fn = (() => string) | (() => number); type R = MyReturnType<Fn>; // string | number,而不是报错
之所以得到联合类型,是因为条件类型被分发成了MyReturnType<() => string> | MyReturnType<() => number>,两边的infer分别推断,结果再合并。这个特性有时是便利,有时是坑。如果不希望分发,标准做法是用一层元组把类型参数包裹起来:
type NoDistribute<T> = [T] extends [infer R][] ? R : never; // 阻止分发的常见写法 type NonDist<T> = [T] extends [() => infer R] ? R : never; type D1 = NonDist<(() => string) | (() => number)>; // 报错前不会分别推断
另一个进阶场景是模板字符串类型与infer的结合。TypeScript允许在模板字符串的各个片段位置放置infer,从而实现字符串层面的模式匹配。经典的例子是从形如getUserName的字符串中提取前缀:
type GetPrefix<S extends string> =
S extends `${infer Head}${Capitalize<string>}` ? Head : never;
type TrimRight<S extends string> =
S extends `${infer Rest} ` ? TrimRight<Rest> : S;
// 注意:模板中infer匹配贪婪,实际去空格需用 ${infer R}${' '} 精确拆分需要注意模板字符串中的infer默认是贪婪匹配,当模板里有多个infer时,前面的infer会尽可能多吃字符,最后一个infer只拿到剩余部分。理解这一点对写复杂的字符串解析类型非常重要。
四、常见误区与最佳实践
第一个常见误区是在extends右侧写了一个永远不会匹配的模板,导致infer永远不被求值,条件类型总是落入false分支。比如想提取数组元素却写成了T extends infer E[] ? E : never,这里的问题是裸的infer E[]缺少约束方向,正确写法应该是给T加上约束或者直接写T extends (infer E)[] ? E : never。
第二个误区是忘记了infer只在true分支可用这条铁律。有些人在false分支里也尝试引用推断出来的变量,结果编译器直接报错。正确的处理方式是在false分支里换一个匹配模板继续尝试,形成多个条件类型串联的结构,这也是许多类型体操代码层层嵌套的由来:
type Unwrap<T> =
T extends Promise<infer V> ? Unwrap<V> :
T extends Array<infer E> ? Unwrap<E> :
T extends { value: infer V } ? Unwrap<V> :
T;最后一个实践建议:给条件类型的类型参数加上合适的约束。约束越精确,编译器能提供的提示越多,也能避免匹配模板写出无意义的形态。同时在复杂推断逻辑上尽量添加类型测试(比如用Expect<Equal<...>>的辅助类型)来固化行为,防止后续TypeScript版本升级带来的细微变化悄悄破坏你的类型工具。理解了infer必须在条件位置做匹配、在true分支做消费这套规则后,条件类型就从一个黑盒变成了可以随心拆解的积木。
TypeScript条件类型类型推断修改时间:2026-09-10 12:34:47