Shift
大约 2 分钟
Shift
题目
实现类型版本的 Array.shift。
type Result = Shift<[3, 2, 1]> // [2, 1]解答
思路
Array.shift 移除数组的第一个元素并返回剩余元素。在 TypeScript 类型系统中,俺们可以利用 可变元组类型(TS 4.0 引入)来解构元组并丢弃第一个元素。
核心思路:如果俺们能将元组模式匹配为 [First, ...Rest],那么 Rest 就是俺们想要的结果。
最终解法
type Shift<T extends any[]> = T extends [any, ...infer Rest] ? Rest : never逐步分析:
T extends any[]—— 约束T为数组/元组T extends [any, ...infer Rest]—— 模式匹配:丢弃第一个元素,将剩余部分推断为Rest? Rest—— 返回尾部: never—— 如果元组为空(没有第一个元素可移除),返回never
测试用例
type Result1 = Shift<[3, 2, 1]> // [2, 1]
type Result2 = Shift<['a', 'b', 'c']> // ['b', 'c']
type Result3 = Shift<[1]> // []
type Result4 = Shift<[]> // never备选:空元组返回 []
根据语义需求,俺们可能希望空输入返回空元组而不是 never:
type Shift<T extends any[]> = T extends [any, ...infer Rest] ? Rest : []这更符合 JavaScript 运行时行为 —— [].shift() 返回 undefined,但数组本身仍为 []。
深入分析
可变元组类型
TypeScript 4.0 引入了通过 infer 进行强大的元组解构:
type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never
type Tail<T extends any[]> = T extends [any, ...infer T] ? T : never这些是元组操作的基础构建块。Shift 本质上就是 Tail。
为什么用 [any, ...infer Rest] 而不是 [infer _First, ...infer Rest]?
两者都能工作,但 [any, ...] 更明确地表达了「俺们故意丢弃第一个元素」的意图。用 infer _First 也可以绑定一个从不使用的名称。
与 Pop 的关系
Pop 移除最后一个元素:T extends [...infer Rest, any] ? Rest : never
Shift 移除第一个元素:T extends [any, ...infer Rest] ? Rest : never
两者是对称的 —— 相同的模式,元组的不同端。
类型保留
Shift 完整保留剩余元素的类型:
type T = [string, number, boolean]
type Shifted = Shift<T> // [number, boolean]结果是一个真正的元组,每个位置都有独立的精确类型,而不是 (string | number | boolean)[]。
社区验证解法
GitHub issue 中的社区解法使用了相同的 infer 模式:
type Shift<T extends any[]> = T extends [any, ...infer Rest] ? Rest : never这是规范的、惯用的 TypeScript 解答。
要点总结
- 可变元组推断 ——
T extends [any, ...infer Rest]是丢弃元组首元素的标准模式 - 元组位置的
infer—— TypeScript 4.0+ 可以将剩余元素推断为完整的元组类型 - 与 Pop 的对称性 —— Shift/Pop 是镜像模式:一个从前面取,一个从后面取
- 类型保真 —— 剩余元素保留其精确的位置类型
- 空元组处理 —— 根据语义需求选择
never还是[]
