qrcode

关注全栈修仙之路,一起学习进阶!

TypeScript 元组类型

Welcome to the Mastering TypeScript series. This series will introduce the core knowledge and techniques of TypeScript in the form of animations. Let’s learn together! Previous articles are as follows:

阅读须知:本文示例的运行环境是 TypeScript 官网的 Playground,对应的编译器版本是 v3.8.3

一、元组类型简介

众所周知,数组一般由同种类型的值组成,但有时我们需要在单个变量中存储不同类型的值,这时候我们就可以使用元组。在 JavaScript 中是没有元组的,元组是 TypeScript 中特有的类型,其工作方式类似于数组。

元组可用于定义具有有限数量的未命名属性的类型。每个属性都有一个关联的类型。使用元组时,必须提供每个属性的值。为了更直观地理解元组的概念,我们来看一个具体的例子:

1
2
let tupleType: [string, boolean];
tupleType = ["Semlinker", true];

在上面代码中,我们定义了一个名为 tupleType 的变量,它的类型是一个类型数组 [string, boolean],然后我们按照正确的类型依次初始化 tupleType 变量。与数组一样,我们可以通过下标来访问元组中的元素:

1
2
console.log(tupleType[0]); // Semlinker
console.log(tupleType[1]); // true

在元组初始化的时候,如果出现类型不匹配的话,比如:

1
tupleType = [true, "Semlinker"]

此时,TypeScript 编译器会提示以下错误信息:

1
2
[0]: Type 'true' is not assignable to type 'string'.
[1]: Type 'string' is not assignable to type 'boolean'.

很明显是因为类型不匹配导致的。在元组初始化的时候,我们还必须提供每个属性的值,不然也会出现错误,比如:

1
tupleType = ["Semlinker"];

此时,TypeScript 编译器会提示以下错误信息:

1
Property '1' is missing in type '[string]' but required in type '[string, boolean]'.

二、元组类型的解构赋值

通过前面的介绍,我们已经知道可以通过下标的方式来访问元组中的元素,当元组中的元素较多时,这种方式并不是那么便捷。其实元组也是支持解构赋值的:

1
2
3
4
let employee: [number, string] = [1, "Semlinker"];
let [id, username] = employee;
console.log(`id: ${id}`);
console.log(`username: ${username}`);

以上代码成功运行后,控制台会输出以下消息:

1
2
id: 1
username: Semlinker

这里需要注意的是,在解构赋值时,如果解构数组元素的个数是不能超过元组中元素的个数,否则也会出现错误,比如:

1
2
let employee: [number, string] = [1, "Semlinker"];
let [id, username, age] = employee;

在以上代码中,我们新增了一个 age 变量,但此时 TypeScript 编译器会提示以下错误信息:

1
Tuple type '[number, string]' of length '2' has no element at index '2'.

很明显元组类型 [number, string] 的长度是 2,在位置索引 2 处不存在任何元素。

三、元组类型的可选元素

与函数签名类型,在定义元组类型时,我们也可以通过 ? 号来声明元组类型的可选元素,具体的示例如下:

1
2
3
4
5
let optionalTuple: [string, boolean?];
optionalTuple = ["Semlinker", true];
console.log(`optionalTuple : ${optionalTuple}`);
optionalTuple = ["Kakuqo"];
console.log(`optionalTuple : ${optionalTuple}`);

在上面代码中,我们定义了一个名为 optionalTuple 的变量,该变量的类型要求包含一个必须的字符串属性和一个可选布尔属性,该代码正常运行后,控制台会输出以下内容:

1
2
optionalTuple : Semlinker,true
optionalTuple : Kakuqo

那么在实际工作中,声明可选的元组元素有什么作用?这里我们来举一个例子,在三维坐标轴中,一个坐标点可以使用 (x, y, z) 的形式来表示,对于二维坐标轴来说,坐标点可以使用 (x, y) 的形式来表示,而对于一维坐标轴来说,只要使用 (x) 的形式来表示即可。针对这种情形,在 TypeScript 中就可以利用元组类型可选元素的特性来定义一个元组类型的坐标点,具体实现如下:

1
2
3
4
5
6
7
8
9
type Point = [number, number?, number?];

const x: Point = [10]; // 一维坐标点
const xy: Point = [10, 20]; // 二维坐标点
const xyz: Point = [10, 20, 10]; // 三维坐标点

console.log(x.length); // 1
console.log(xy.length); // 2
console.log(xyz.length); // 3

四、元组类型的剩余元素

元组类型里最后一个元素可以是剩余元素,形式为 ...X,这里 X 是数组类型。 剩余元素代表元组类型是开放的,可以有零个或多个额外的元素。 例如,[number, ...string[]] 表示带有一个 number 元素和任意数量string 类型元素的元组类型。为了能更好的理解,我们来举个具体的例子:

1
2
3
4
type RestTupleType = [number, ...string[]];
let restTuple: RestTupleType = [666, "Semlinker", "Kakuqo", "Lolo"];
console.log(restTuple[0]);
console.log(restTuple[1]);

此外,在定义函数时我们也可以使用剩余参数语法,比如:

1
2
3
4
5
6
7
8
function useTupleAsRest(...args: [number, string, boolean]) {
let [arg1, arg2, arg3] = args;
console.log(`arg1: ${arg1}`);
console.log(`arg2: ${arg2}`);
console.log(`arg3: ${arg3}`);
}

useTupleAsRest(1, "Semlinker", true);

在上面代码中,我们使用剩余参数语法定义了一个名为 useTupleAsRest 的函数,该函数的参数类型是元组类型,即 [number, string, boolean]。在该函数的第一行中,我们把 args 元组中保存的值解构赋值给 arg1arg2arg3 这三种不同类型的变量,最后我们分别输出三个变量的值。

五、元组类型的展开表达式

在函数调用中,若最后一个参数是元组类型的展开表达式,那么这个展开表达式相当于元组元素类型的离散参数序列。因此,下面的调用都是等价的:

1
2
3
4
5
const args: [number, string, boolean] = [42, "hello", true];

foo(42, "hello", true);
foo(args[0], args[1], args[2]);
foo(...args);

这里为了让大家能更好的理解,我们来举一个实际的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Point3D = [number, number, number];

const drawPoint = (...point3D: Point3D) => {
console.log(point3D);
};

const xyzCoordinate: Point3D = [10, 20, 30];

// 使用字面量的形式设置值
drawPoint(10, 20, 30);

// 使用索引的方式来访问xyzCoordinate元组中的元素
drawPoint(xyzCoordinate[0], xyzCoordinate[1], xyzCoordinate[2]);

// 使用展开语法来访问xyzCoordinate元组中的元素
drawPoint(...xyzCoordinate);

在这个示例中,我们首先使用 type 定义了 Point3D 元组类型,用于表示一个三维的坐标点,然后我们利用剩余参数的语法,定义了一个 drawPoint 函数,用于输出三维坐标点的坐标值,接着定义一个名为 xyzCoordinate 元组类型变量并初始化,最后使用三种不同的形式来调用 drawPoint 函数。

六、只读的元组类型

TypeScript 3.4 还引入了对只读元组的新支持。我们可以为任何元组类型加上 readonly 关键字前缀,以使其成为只读元组。具体的示例如下:

1
const point: readonly [number, number] = [10, 20];

在使用 readonly 关键字修饰元组类型之后,任何企图修改元组中元素的操作都会抛出异常:

1
2
3
4
5
6
7
8
// Cannot assign to '0' because it is a read-only property.
point[0] = 1;
// Property 'push' does not exist on type 'readonly [number, number]'.
point.push(0);
// Property 'pop' does not exist on type 'readonly [number, number]'.
point.pop();
// Property 'splice' does not exist on type 'readonly [number, number]'.
point.splice(1, 1);

七、参考资源


欢迎小伙伴们订阅全栈修仙之路,及时阅读 TypeScript、Node/Deno、Angular 技术栈最新文章。

qrcode