TypeScript的条件类型(Conditional Type)配合infer关键字,构成了类型编程中最强大的组合。大多数教程会告诉你infer可以提取类型,但当待推断的位置出现在元组(Tuple)上下文中时,推断结果往往和直觉不一致:明明期望得到一个元组,结果却是一个数组;明明只写了一个infer,编译器却给出联合类型。理解这套分配规则,是编写元组工具类型的前提。

条件类型中的infer基础回顾
先看最简单的形式。T extends U ? X : Y表示如果T可以赋值给U,则结果为X,否则为Y。当U中出现infer R时,TypeScript会尝试从T中“抠出”与R对应的部分。例如经典的ReturnType:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never; type A = ReturnType<() => number>; // number</code>
这里的规则很直白:一个infer位置只对应一个类型。但在元组场景下,一个infer位置可能对应“一段”长度不确定的元素序列,这正是问题的根源。TypeScript为了处理这种不确定性,引入了两种策略:一种是把推断结果收敛为元组类型,另一种是收敛为数组类型,选择哪种取决于infer出现在什么位置。
还有一个容易被忽略的规则需要提前说明:如果条件类型作用的T本身是一个泛型并且传入联合类型,条件类型会分布式展开。元组本身不会触发分布,但如果推断过程中产生了联合类型,结果也会随之变化。这两套机制叠加,构成了元组推断的复杂性。
infer在元组元素位置与剩余位置的两种形态
第一种形态是infer出现在固定元素位置,此时TypeScript倾向推断出元组类型:
type Head<T extends any[]> = T extends [infer F, ...any[]] ? F : never; type H = Head<[string, number, boolean]>; // string type Shift<T extends any[]> = T extends [any, ...infer Rest] ? Rest : []; type S = Shift<[string, number, boolean]>; // [number, boolean]</code>
注意Shift中的infer Rest处于剩余元素(rest element)位置。由于它前面的元素是固定的一个,TypeScript能确定边界,因此Rest被推断为元组[number, boolean]而不是number[]。这是控制推断结果的第一条规律:当剩余位置的infer两侧至少有一侧存在固定元素时,推断结果保持元组结构。
第二种形态是完全开放的位置:
type Flatten<T> = T extends Array<infer E> ? E : T; type F1 = Flatten<[string, number]>; // string | number type Wrap<T extends any[]> = T extends [...infer R] ? R : never; type W1 = Wrap<[string, number]>; // [string, number]</code>
Flatten中infer E出现在数组的元素类型位置,TypeScript会收集所有元素类型并合并为联合类型,得到string | number。而Wrap里[...infer R]虽然看起来开放,但由于R整体代表整个元组,它仍然被推断为元组。区分这两者的关键在于:infer是描述“元素序列”还是描述“单个元素的类型”。
多infer切割元组与推断位置的协变行为
当元组中同时出现多个infer时,TypeScript会尽量为每个推断变量分配一段元素序列。经典的Last实现展示了这一点:
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
type L1 = Last<[string, number, boolean]>; // boolean
type Chunk<T extends any[], N extends number> =
T extends [...infer Front, infer Tail]
? Front['length'] extends N
? [T, []]
: never
: never;</code>这里出现了第二条重要规律:剩余位置的infer采用贪婪或非贪婪的边界推断,且多个剩余infer不能同时开放。如果你写T extends [...infer A, ...infer B],编译器会直接报错,因为无法确定边界在哪里。只有当至少一个推断变量被固定元素锚定时,切割才有唯一解。
关于协变与双变,需要理解infer的上下文:出现在元组的剩余位置时,推断是协变的,意味着子元组也能匹配;出现在函数参数位置时(逆变位置),多个候选类型会推断为交叉类型。这个特性常被用来合并函数重载:
type UnionToIntersection<U> =
(U extends any ? (arg: U) => void : never) extends (arg: infer I) => void
? I : never;
type R = UnionToIntersection<{ a: 1 } | { b: 2 }>; // { a: 1 } & { b: 2 }</code>同一个infer I面对多个候选时,正序位置取联合、逆序位置取交叉,这条规则同样适用于元组中嵌套函数的场景。
实际编写元组工具类型时的注意事项
掌握了上述规律后,编写复杂工具类型时要养成几个习惯。第一,优先用固定元素锚定边界,比如提取尾部时写[...any[], infer L]而不是对数组做递归取值。第二,递归处理元组时注意性能,TypeScript对递归深度有上限(大约50层左右的实例化深度),处理超长元组应考虑分治。第三,当无法保证输入一定是元组时,要显式区分数组与元组两种情况:
type IsTuple<T> = T extends readonly [any, ...any[]] | readonly [...any, any] ? true : false; type Reverse<T extends readonly any[]> = T extends readonly [infer F, ...infer R] ? [...Reverse<R>, F] : T; type Rev = Reverse<['a', 'b', 'c']>; // ['c', 'b', 'a']</code>
Reverse是元组递归的标准范式:用[infer F, ...infer R]把元组拆成头部和尾部,递归处理尾部再拼接。由于F和R的边界由结构确定,推断结果始终是元组,不会退化成数组。
最后要注意readonly修饰符的影响。readonly [string, number]不能匹配[infer F, ...infer R],因为可变元组与只读元组之间的兼容是单向的。处理可能包含只读元组的场景时,条件类型的模式一侧也要加上readonly,否则匹配会静默失败并落入false分支,这类bug往往难以察觉,因为编译器不会给出任何提示。
总结来看,元组场景下的infer推断遵循三条核心规则:固定位置推断为元组、元素类型位置推断为联合、多剩余推断必须有锚定。把这三条规则记牢,再配合递归模式,绝大多数元组级类型体操都能顺利拆解。
TypeScript条件类型类型推断修改时间:2026-09-12 03:16:34