数组与元组

2026-09-09 00:00    #typescript  

数组是"同质、长度不定",元组是"定长、每个位置的类型固定"。选择标准是:位置是否有独立含义。

 1// 数组 vs 元组:怎么选
 2
 3// 数组:长度不定,所有元素同类型
 4const ids: number[] = [1, 2, 3];
 5
 6// 元组:定长,每个位置的类型固定
 7const point: [number, number] = [1, 2];
 8const entry: [string, number] = ["age", 18];
 9
10// 反例:用数组表达"位置有意义"的数据,会丢失约束
11const badPoint: number[] = [1, 2];
12badPoint.push(3);        // 允许,但 3 个坐标没有意义
13badPoint[0] = 0;         // 允许
14
15const goodPoint: [number, number] = [1, 2];
16// 注意:定长元组仍然允许 push!length 类型只是"至少"信息
17goodPoint.push(3);
18// 只有 readonly 元组才真的阻止修改
19const roPoint: readonly [number, number] = [1, 2];
20// @ts-expect-error TS2339: readonly 元组没有 push
21roPoint.push(3);
22
23// 反例:用元组表达"同质列表",超过 3 个元素就很难读
24const badList: [string, string, string, string] = ["a", "b", "c", "d"];
25// goodList 更合适
26const goodList: string[] = ["a", "b", "c", "d"];
27
28// 元组最适合的场景:函数返回多个值
29function minMax(nums: number[]): [number, number] {
30  return [Math.min(...nums), Math.max(...nums)];
31}
32const [min, max] = minMax([1, 2, 3]);
33
34// 以及固定形状的键值对
35const entries: [string, number][] = Object.entries({ a: 1, b: 2 });
36
37// 元组超过 3 个元素时,对象更可读
38type BadUser = [string, number, string, boolean];
39type GoodUser = { name: string; age: number; city: string; active: boolean };
40
41const badUser: BadUser = ["a", 1, "b", true];
42const goodUser: GoodUser = { name: "a", age: 1, city: "b", active: true };
43// goodUser.age 比 badUser[1] 可读得多
44
45console.log(ids, point, entry, badPoint, goodPoint, roPoint, badList, goodList, min, max, entries, badUser, goodUser);
1[ 1, 2, 3 ] [ 1, 2 ] [ 'age', 18 ] [ 0, 2, 3 ] [ 1, 2, 3 ] [ 1, 2, 3 ] [ 'a', 'b', 'c', 'd' ] [ 'a', 'b', 'c', 'd' ] 1 3 [ [ 'a', 1 ], [ 'b', 2 ] ] [ 'a', 1, 'b', true ] { name: 'a', age: 1, city: 'b', active: true }

什么时候用元组

适合:

不适合:

1type Bad = [string, number, string, boolean];
2type Good = { name: string; age: number; city: string; active: boolean };
3
4goodUser.age;    // 比 badUser[1] 可读得多

元组的各种形态

 1// 元组的各种形态
 2
 3// 1. 基本元组
 4const pair: [string, number] = ["a", 1];
 5
 6// 2. 可选元素:必须放在最后
 7const optional: [string, number?] = ["a"];
 8const optional2: [string, number?] = ["a", 1];
 9
10// 3. 剩余元素:定义最小长度
11const atLeastOne: [string, ...string[]] = ["a"];
12const atLeastOne2: [string, ...string[]] = ["a", "b", "c"];
13// @ts-expect-error TS2322: 至少需要一个元素
14const empty: [string, ...string[]] = [];
15
16// 4. 混合:前面固定,后面不定
17const mixed: [number, boolean, ...string[]] = [1, true, "a", "b"];
18
19// 5. 只读元组
20const roTuple: readonly [string, number] = ["a", 1];
21// @ts-expect-error TS2540: Cannot assign to '0' because it is a read-only property.
22roTuple[0] = "b";
23
24// 6. 元组数组
25const pairs: [string, number][] = [["a", 1], ["b", 2]];
26
27// 7. 可选元素的等价写法
28// [number, number?] 等价于 [number, number] | [number]
29type A = [number, number?];
30type B = [number, number] | [number];
31const a1: A = [1];
32const b1: B = [1];
33
34// 8. 命名元组元素(TS 4.0+):只是文档作用,不改变类型
35type Range = [start: number, end: number];
36const range: Range = [1, 10];
37// 命名会显示在类型提示里
38
39// 9. 嵌套元组
40const matrix: [[number, number], [number, number]] = [[1, 2], [3, 4]];
41
42// 10. 解构
43const [first, second] = pair;
44const [head, ...tail] = atLeastOne2;
45
46// 元组的 length 是字面量类型
47type Len = typeof pair["length"];    // 2
48const len: Len = 2;
49
50console.log(pair, optional, optional2, atLeastOne, atLeastOne2, mixed, roTuple, pairs);
51console.log(a1, b1, range, matrix, first, second, head, tail, len);
1[ 'a', 1 ] [ 'a' ] [ 'a', 1 ] [ 'a' ] [ 'a', 'b', 'c' ] [ 1, true, 'a', 'b' ] [ 'b', 1 ] [ [ 'a', 1 ], [ 'b', 2 ] ]
2[ 1 ] [ 1 ] [ 1, 10 ] [ [ 1, 2 ], [ 3, 4 ] ] a 1 a [ 'b', 'c' ] 2
形态写法说明
基本[string, number]位置类型固定
可选元素[string, number?]必须放最后;等价于 [string, number] | [string]
剩余元素[string, ...string[]]定义最小长度
混合[number, boolean, ...string[]]前面固定,后面不定
只读readonly [string, number]不可修改
命名元素[start: number, end: number]仅用于提示,不改变类型

length 是字面量类型

1const pair: [string, number] = ["a", 1];
2type Len = typeof pair["length"];    // 2

这个特性可以用来做类型层面的长度检查。

元组的 length 不等于"不可 push"

这是最容易误解的一点。定长元组仍然允许 push:

1const goodPoint: [number, number] = [1, 2];
2goodPoint.push(3);        // 编译通过!

length: 2 只表示"已知至少有这两个位置",不阻止运行时增长。真正要阻止修改必须用 readonly:

1const roPoint: readonly [number, number] = [1, 2];
2roPoint.push(3);
3// error TS2339: Property 'push' does not exist on type 'readonly [number, number]'.

只读元组的索引赋值报:

1error TS2540: Cannot assign to '0' because it is a read-only property.

数组的两种写法

1const a: number[] = [1, 2];        // 简写
2const b: Array<number> = [1, 2];   // 泛型写法

完全等价。选一种保持一致即可。区别只在于复杂类型下 T[] 的可读性会下降:

1const x: (string | number)[] = [];      // 需要括号
2const y: Array<string | number> = [];   // 更清楚

只读数组

1const ro: readonly number[] = [1, 2, 3];
2ro.push(4);
3// error TS2339: Property 'push' does not exist on type 'readonly number[]'.

readonly T[]、ReadonlyArray<T>、Readonly<T[]> 三种写法等价。只读数组的用处是表达"这个函数不会修改传入的数组":

1function sum(nums: readonly number[]): number {
2  return nums.reduce((a, b) => a + b, 0);   // 保证不修改入参
3}
4
5sum([1, 2, 3]);
6const arr = [1, 2];
7sum(arr);          // 可变数组可以传给只读参数

反过来不行:readonly number[] 不能赋给 number[],因为那样就绕过了保护。

相关