# 基操-元组遍历的黑科技

元组是 TS 体操里与字符串并列的两大"递归主战场"。相比字符串只有"从头拆一个字符"这种相对死板的模板匹配,元组有更多玩法。本文汇总五种最常用的元组遍历技巧。

# 技巧 1:拆首取尾 [infer F, ...infer R]

最基础的模板。配合条件类型递归,就能依次处理每一项:

type ForEach<T extends any[]> = T extends [infer F, ...infer R]
  ? [F, ...ForEach<R>]
  : [];

变体:

// 只取第一个
type First<T extends any[]> = T extends [infer F, ...any] ? F : never;

// 只取剩下
type Rest<T extends any[]> = T extends [any, ...infer R] ? R : [];

递归出口一般是 []never

# 技巧 2:拆尾取首 [...infer R, infer L]

对称的另一端:

type Last<T extends any[]> = T extends [...any, infer L] ? L : never;

type Pop<T extends any[]> = T extends [...infer R, any] ? R : [];

// 反转
type Reverse<T extends any[]> = T extends [...infer R, infer L]
  ? [L, ...Reverse<R>]
  : [];

"从头递归拼尾"和"从尾递归拼头"能实现同样效果,写起来看哪头更自然就用哪种。

# 技巧 3:按索引映射(mapped type 应用在元组上)

这是初学者最容易忽视但超级好用的一条:用 [K in keyof T] 遍历元组时,TS 会保留元组形态,把每一项单独映射。

// 每一项套一层数组
type Wrap<T extends any[]> = {
  [K in keyof T]: [T[K]];
};

type R = Wrap<[1, 2, 3]>; // [[1], [2], [3]]

好处是:不用写递归,比 [infer F, ...infer R] 写法省一大截代码。适合"对每一项独立改造,不依赖前后项"的场景。

经典应用:

// 把每一项包成函数参数
type ToFunctions<T extends any[]> = {
  [K in keyof T]: (arg: T[K]) => void;
};

注意:

  • keyof T 在元组上会把 number/length/toString/...一堆数组原型方法也算进去,但 [K in keyof T] 这种语法 TS 只会保留"数字索引"那部分。
  • 产出的类型仍保持元组性(length 不变),这是和"转成数组类型"最大的区别。

# 技巧 4:T['length'] 做计数

元组的 length 属性在类型层拿到的是具体字面量 number,这是 TS 所有"数值运算"的基石:

type A = [1, 2, 3]['length']; // 3

// 计数器模式:每递归一轮往辅助元组里 push 一个元素
type CountA<S extends string, C extends any[] = []> = S extends `a${infer R}`
  ? CountA<R, [...C, 1]>
  : C['length'];

几乎所有"想在类型里算一个数"的场景(加减乘除、判断长度、跳过前 N 项)都走这条路。细节见 加减乘除

# 技巧 5:构造一个指定长度的元组

反向操作:给定 N,造一个 length = N 的元组,通常做辅助数据结构。

type BuildTuple<N extends number, R extends any[] = []> = R['length'] extends N
  ? R
  : BuildTuple<N, [...R, any]>;

type T = BuildTuple<5>; // [any, any, any, any, any]

这是 medium/7544 的直接解法,也是后续"加法 / 乘法"等操作的起点。

# 技巧 6:剩余参数放在中间 / 两端

TS 4.2 起,元组的 rest 位置非常灵活:

// rest 在中间
type Middle<T> = T extends [any, ...infer M, any] ? M : [];
type R = Middle<[1, 2, 3, 4]>; // [2, 3]

// 拆前两项
type Two<T> = T extends [infer A, infer B, ...infer R] ? [A, B, R] : never;

通过调整 rest 的位置,可以一次抓取任意"切片"。

# 技巧 7:元组 ↔ 联合

type T = [1, 2, 3];

// 元组 -> 联合
type U = T[number]; // 1 | 2 | 3

// 联合 -> 元组(逆变推断,hard 技巧)
// 见 hard/730-联合转元组

T[number] 能让你在不递归的情况下"看一眼所有项",但失去了顺序信息;递归模板保顺序但成本更高。看题意选择。

# 实战组合:对每一项带索引改造

把"mapped type 映射 + length 计数"组合起来,就能拿到"当前项的索引":

// 给每一项附加它的索引
type WithIndex<T extends any[]> = {
  [K in keyof T]: [K, T[K]];
};

type R = WithIndex<['a', 'b', 'c']>;
// [['0', 'a'], ['1', 'b'], ['2', 'c']]

这里 K 是字符串形式的数字,想变回 number 还得 K extends${infer N extends number}? N : never

另一种拿索引的思路是"递归时维护一个辅助元组充当计数器",见 加减乘除

# 速查表

目的 模板
头递归 T extends [infer F, ...infer R] ? ... : ...
尾递归 T extends [...infer R, infer L] ? ... : ...
取首 T extends [infer F, ...any] ? F : never
取尾 T extends [...any, infer L] ? L : never
切片中段 T extends [any, ...infer M, any] ? M : []
每项映射 { [K in keyof T]: F<T[K]> }
元组转联合 T[number]
长度计数 T['length']
构造定长元组 递归 push,直到 R['length'] extends N

# 被谁用到

看到元组题先反射"头递归 / 尾递归 / mapped type / length 计数"这四条路径,大半都能对上套。

Last Updated: 2026/4/19 00:26:40