# 基操-元组遍历的黑科技
元组是 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 |
# 被谁用到
- 头尾递归:medium/3192-Reverse, medium/14-First, medium/15-Last。
- Mapped 映射:medium/3196-反转入参,medium/191-追加参数。
T['length']计数:medium/298-计算字符的长度, medium/2257-减一。- 构造定长元组:medium/7544-构造一个给定长度的元组, medium/4518-fill。
- 切片:extreme/216-实现 slice。
看到元组题先反射"头递归 / 尾递归 / mapped type / length 计数"这四条路径,大半都能对上套。
← 基操-类型转换大集合 进阶-计数-加减乘除 →