Yuzhe's Blog

yuzhes

Shift

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

逐步分析:

  1. T extends any[] —— 约束 T 为数组/元组
  2. T extends [any, ...infer Rest] —— 模式匹配:丢弃第一个元素,将剩余部分推断为 Rest
  3. ? Rest —— 返回尾部
  4. : 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 解答。

要点总结

  1. 可变元组推断 —— T extends [any, ...infer Rest] 是丢弃元组首元素的标准模式
  2. 元组位置的 infer —— TypeScript 4.0+ 可以将剩余元素推断为完整的元组类型
  3. 与 Pop 的对称性 —— Shift/Pop 是镜像模式:一个从前面取,一个从后面取
  4. 类型保真 —— 剩余元素保留其精确的位置类型
  5. 空元组处理 —— 根据语义需求选择 never 还是 []