跳到主要内容

第 14 章 · 元组与数组体操

读完本章你会
  • 掌握元组的「拆」与「拼」:[F, ...R][...R, L],实现增删改查
  • 实现元组反转、弹出、位移等「顺序变换」
  • 用累积参数实现查找(IndexOf)与去重(Unique
  • 理解「元组长度 ⇄ 数字」的互相转换,这是第 17 章数学体操的地基
  • 学会判断「是不是元组」(IsTuple

14.1 元组的基本拆拼

第 5 章见过 [F, ...R] 拆头、[...R, L] 拆尾。这两个动作是本章所有操作的原语:

// 拆头:取第一项 + 剩余
type PopHead<T extends any[]> = T extends [infer F, ...infer R] ? F : never
type FirstOf<[1, 2, 3]> // 1

// 拆尾:取最后一项 + 其余
type PopTail<T extends any[]> = T extends [...infer R, infer L] ? L : never
type LastOf<[1, 2, 3]> // 3

// 拼头 / 拼尾(第 5 章的 Push / Unshift 正是它们)
type Push<T extends any[], U> = [...T, U]
type Unshift<T extends any[], U> = [U, ...T]
拆和拼是一对

[F, ...R] 拆出来的东西,用 [F, ...R] 就能拼回去。所有元组变换都是「拆几步 → 重组」


14.2 顺序变换:Pop / Shift / Reverse

14.2.1 Pop 与 Shift:去掉一头

// 去掉最后一项
type Pop<T extends any[]> = T extends [...infer R, any] ? R : []

type A = Pop<[1, 2, 3]> // [1, 2]

// 去掉第一项
type Shift<T extends unknown[]> = T extends [unknown, ...infer R] ? R : []

type B = Shift<[1, 2, 3]> // [2, 3]

注意 Pop[..., infer R, any] 把「最后一个位置」占掉,剩下 R——这就是「弹出」的模型。

14.2.2 Reverse:累积参数反转

用「累积 + 头插」反转,是最直观的写法:

type Reverse<T extends any[], R extends any[] = []> =
T extends [infer F, ...infer Rest]
? Reverse<Rest, [F, ...R]> // 把 F 插到 R 前面
: R

type A = Reverse<['a', 'b', 'c']> // ['c', 'b', 'a']
累积参数的「方向」

[F, ...R](头插)会反转[...R, F](尾插)会保持顺序。想清楚你要哪种顺序,就选哪种拼法。


14.3 查找与去重

14.3.1 IndexOf:位置 + Equal

IndexOf<T, U> 返回 UT 中的下标(没有则 -1)。用累积参数数下标:

type Equal<X, Y> = ( /* 第 12 章的严格相等 */ )

type IndexOf<T, U, C extends any[] = []> =
T extends [infer F, ...infer Rest]
? Equal<F, U> extends true
? C['length'] // 找到了 → 返回当前计数
: IndexOf<Rest, U, [...C, any]> // 没找到 → 计数 +1,继续
: -1

type A = IndexOf<[2, 6, 3], 3> // 2
type B = IndexOf<[2, 6, 3], 9> // -1
用 C['length'] 数数

C extends any[] = [] 每递归一层就 [...C, any],于是 C['length'] 就是「当前到第几层」——这正是「元组长度当计数器」的用法,第 17 章的数学全建立在它上面。

14.3.2 Unique:去重(复习第 13 章)

type Includes<Arr extends any[], U> = /* 13.3.1 */

type Unique<T extends any[], R extends any[] = []> =
T extends [infer F, ...infer Rest]
? Includes<R, F> extends true
? Unique<Rest, R>
: Unique<Rest, [...R, F]>
: R
本章 6 题的「难度阶梯」

Pop/Shift/Reverse纯拆拼IndexOf 加了 Equal + 计数UniqueIncludes + 累积IsTuple 则是元组 vs 数组的判定。由易到难,正好把本章的手法串一遍。


14.4 IsTuple:元组 vs 数组

「是不是元组」的判定很实用。元组和数组的区别在于 length

  • 元组的 length字面量125……)
  • 数组的 lengthnumber
type IsTuple<T> =
[T] extends [never] // 先排除 never
? false
: T extends readonly any[] // 必须长得像数组/元组
? number extends T['length'] ? false : true // length 是 number → 数组;是字面量 → 元组
: false

type A = IsTuple<[1, 2]> // true
type B = IsTuple<number[]> // false(length 是 number)
type C = IsTuple<{ length: 1 }> // false(不是数组形状)
为什么先判 never

never 会「extends 任何东西」,如果直接走后面的判断,IsTuple<never> 会误判。先用 [T] extends [never] 挡一层是标准防御。


14.5 元组长度 ⇄ 数字

这是本章最重要的一节,也是第 17 章的地基。

14.5.1 数字 → 元组:BuildTuple

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

type A = BuildTuple<3> // [any, any, any]

14.5.2 元组 → 数字:直接取 length

type B = [any, any, any]['length'] // 3

14.5.3 数字运算 = 元组长度运算

// 两个数字相加:把两个元组拼起来取长度
type Add<A extends number, B extends number> =
[...BuildTuple<A>, ...BuildTuple<B>]['length']

type S = Add<2, 3> // 5

// 两个数字相减:长的元组吞掉短的,剩多少就是差
type Subtract<M extends number, S extends number> =
BuildTuple<M> extends [...BuildTuple<S>, ...infer R] ? R['length'] : never

type D = Subtract<5, 2> // 3
元组法的边界

这种「用元组长度当数字」的办法,上限受递归深度限制(第 13 章 13.5)。小数字没问题,MinusOne<1000> 附近就开始吃力,再大就要换字符串算术(第 17 章)。理解它的原理,比记住上限更重要。


本章小结

  • 元组原语:[F, ...R] 拆头、[...R, L] 拆尾、[...T, U] 拼尾、[U, ...T] 拼头
  • Pop/Shift/Reverse 是纯拆拼;Reverse 用「头插累积」反转
  • IndexOf = 拆 + Equal + 累积计数;Unique = Includes + 累积
  • IsTuple 用「length 是字面量还是 number」判定
  • 元组长度 ⇄ 数字互相转换,Add/Subtract 就此实现——第 17 章的地基

本章练习

挑战题

题号题目难度考察点
00016排除最后一项medium[...R, any] 拆尾
03192Reversemedium头插累积
03062Shiftmedium[unknown, ...R] 拆头
04484IsTuplemediumlength 判定
05153IndexOfmediumEqual + 计数
05360UniquemediumIncludes + 累积

代码练习

  1. Pop/Shift 变体:实现 PopShift 后,再实现 PushUnshift,验证四个方向都能「加/减一头」。

  2. IndexOf 计数:不照抄 14.3.1,自己用累积参数实现 IndexOf,验证 IndexOf<[string, 1, number], number>2

  3. Reverse 双版本:分别用 [F, ...R](头插)和 [...R, F](尾插)实现反转,观察结果差异,说明为什么头插才反转。

  4. Add / Subtract:实现 Add<A, B>Subtract<M, S>(14.5.3),验证 Add<4, 7>11Subtract<10, 4>6

  5. IsTuple 组合:用 IsTupletype FirstOr<T, D>:如果是元组返回第一项,否则返回默认值 D(提示:IsTuple<T> extends true ? ... : D)。