Menu

TypeScript 元组(tuple):语法、具名、可选与剩余元素

TypeScript 元组是一个元素个数固定、每个位置类型已知的数组,例如 [string, number]。本文讲解元组语法、具名元素、可选元素和剩余元素、只读元组与 as const、从函数返回元组,以及元组和数组的区别。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

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 | numberstring
x[5] 的类型string | number编译错误 TS2493
length 的类型number2
类型的顺序不记录记录
运行时的值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 和按索引赋值就都会变成编译错误。

Coddy 编程语言插图

和 Coddy 一起学编程

开始