TypeScript 元组是一个元素个数固定、每个位置都有自己类型的数组。[string, number] 表示恰好两个元素:先是一个字符串,再是一个数字。按照值出现的顺序,把各个类型写在方括号里即可。
在运行时,元组就是普通的 JavaScript 数组。元组额外带来的一切(固定的长度和每个位置的类型)都由编译器检查,然后被擦除。
元组语法
| 元组类型 | 接受的值 | length 的类型 |
|---|---|---|
[string, number] | 恰好一个字符串,再加一个数字 | 2 |
[x: number, y: number] | 同上,加了标签,更易读 | 2 |
[number, number, number?] | 2 个或 3 个数字 | 2 | 3 |
[string, ...number[]] | 一个字符串,后面跟任意个数字 | number |
[...string[], number] | 任意个字符串,最后一个数字 | number |
readonly [number, number] | 不能修改的一对值 | 2 |
[] | 只能是空数组 | 0 |
下面逐一讲解每种形式。值得留意的是 length 的类型:对于固定长度的元组,它是一个字面量类型,所以编译器知道 pair.length 恰好是 2。
编译器检查什么
元组类型固定了元素的个数、顺序以及每个位置的类型。其中任何一项写错都是编译错误:
index.ts(2,7): error TS2322: Type '[string]' is not assignable to type '[string, number]'.
Source has 1 element(s) but target requires 2.
index.ts(3,36): error TS2322: Type 'number' is not assignable to type 'string'.
index.ts(3,40): error TS2322: Type 'string' is not assignable to type 'number'.
index.ts(5,16): error TS2493: Tuple type '[string, number]' of length '2' has no element at index '2'.
普通数组永远发现不了最后一个错误:对于 string[],arr[2] 就是一个 string,只是在运行时恰好是 undefined。
具名元组元素
标签用来说明每个位置的含义。它们完全不影响类型和索引方式,但编辑器会在悬停提示和函数签名提示中显示它们,这让 [number, number] 不再那么难懂。
从 TypeScript 5.2 开始,可以只给部分位置加标签,其余位置不加,例如 [first: string, number]。标签只是写给人看的:[x: number, y: number] 和 [number, number] 是同一个类型,可以互相赋值。
可选元素
在元素类型后面加 ?,这个位置就变成可选的。可选元素必须放在必需元素之后,而且每多一个可选元素,length 的类型就会多一种可能。
读取可选元素得到的是 T | undefined,所以在做算术运算之前,需要在解构模式里给默认值(a = 1)或者先做检查。
剩余元素
剩余元素 ...T[] 表示任意个 T 类型的元素。它可以放在末尾、开头或中间,但每个元组最多只能有一个。
带剩余元素的元组,length 的类型是 number,因为长度不再固定。保持固定的是那些带类型的位置在哪里。
只读元组与 as const
readonly [T, U] 去掉了 push、pop、splice 和按索引赋值,这正是固定长度的值应有的样子。在数组字面量后面写 as const,会推断出一个由字面量类型组成的只读元组。
(typeof SIZES)[number] 把元组变成其元素类型的联合类型,这个模式在 索引访问类型 中有讲解。只读元组不能传给类型为可变元组的参数,所以只读取数据的函数应该接受 readonly [number, number]。
readonly 检查只发生在编译期。在运行时数组并没有被冻结(从输出可以看到,上面的赋值确实执行了),所以如果需要运行时保证,请使用 Object.freeze。
从函数返回元组
把多个值作为元组返回,正是 React 的 useState 的做法(const [value, setValue] = useState(0))。但有个坑:return 中的数组字面量会被推断为数组,而不是元组。
index.ts(9,13): error TS2365: Operator '+' cannot be applied to types 'number | (() => number)' and 'number'.
index.ts(10,1): error TS2349: This expression is not callable.
Not all constituents of type 'number | (() => number)' are callable.
Type 'number' has no call signatures.
这个函数返回的是 (number | (() => number))[],所以解构出来的两个名字都是联合类型。有两种修复方法:标注返回类型,或者加上 as const。
返回元组可以让调用方随意给各部分命名。如果值超过两三个,或者顺序不直观,就改为返回对象:{ count, increment } 本身就说明了含义。
元组作为函数参数
类型为元组的剩余参数可以描述整个参数列表,包括可选参数。内置的工具类型 Parameters<T> 就是用这种方式表示函数参数的。
把元组展开传入函数调用时,每个实参都会按位置做类型检查,而展开一个 (string | number)[] 做不到这一点。
元组与数组的区别
数组 (string | number)[] | 元组 [string, number] | |
|---|---|---|
| 长度 | 任意 | 固定(或由可选元素和剩余元素限定范围) |
x[0] 的类型 | string | number | string |
x[5] 的类型 | string | number | 编译错误 TS2493 |
length 的类型 | number | 2 |
| 类型的顺序 | 不记录 | 记录 |
| 运行时的值 | JavaScript 数组 | 同样的 JavaScript 数组 |
| 典型用途 | 一组同类元素 | 固定的小组合:一对值、坐标、[key, value]、多个返回值 |
内置类型中也会出现元组。Object.entries(obj) 返回 [string, T][],而 Map 是用 [key, value] 元组构造的:
有一个陷阱:可变元组仍然拥有所有数组方法,所以 pair.push(3) 在 [string, number] 上也能通过编译,悄悄地生成一个三元素数组,而它的类型却说只有两个。把元组声明为 readonly 就能堵住这个漏洞。另外,由于类型会被擦除,来自程序外部的数据(JSON、API)在运行时不会按元组类型检查:在信任它之前,先验证它的长度和元素类型。
可变参数元组类型
元组类型可以展开其他元组类型,写作 [...T, ...U]。结合泛型,就能为拼接或前置元素的函数标注类型,同时保留每个位置的类型:
库的类型也大量依赖元组推断:Promise.all([fetchUser(), fetchPosts()]) 会解析为一个元组,每个输入的 Promise 对应一个类型。
常见问题
TypeScript 中的元组是什么?
元组是一种长度固定、每个位置都有自己类型的数组类型:[string, number] 表示恰好两个元素,先是一个字符串,再是一个数字。在运行时它就是普通的 JavaScript 数组;长度和每个位置的类型只在编译期检查。
TypeScript 中元组和数组有什么区别?
像 (string | number)[] 这样的数组类型长度任意,每个元素的类型都相同(同一个联合类型),所以 arr[0] 是 string | number。像 [string, number] 这样的元组长度已知,t[0] 是 string,t[1] 是 number,而 t[2] 会报编译错误。
在 TypeScript 中如何从函数返回元组?
标注返回类型 function f(): [number, string],或者在返回表达式末尾加上 as const,得到一个只读元组。两者都不用的话,return [count, setCount] 会被推断为联合类型的数组,例如 (number | (() => void))[],解构后得到的也都是联合类型。
什么是具名元组元素?
就是给各个位置加上标签,例如 [name: string, age: number]。标签不会改变类型,也不会改变访问方式(仍然是 t[0]),但编辑器会在悬停提示中显示它们;参数类型为元组的函数,其参数提示里也会显示。可选元素和剩余元素同样可以加标签:[x: number, y?: number]、[head: string, ...rest: number[]]。
TypeScript 元组可以 push 吗?
可变元组可以:push 能通过编译,因为元组继承了数组的方法,尽管这会破坏固定长度。把元组声明为 readonly(或用 as const 创建),push、pop 和按索引赋值就都会变成编译错误。