元组类型

元组(Tuple)表示由有限元素构成的有序列表。在 JavaScript 中,没有提供原生的元组数据类型。TypeScript 对此进行了补充,提供了元组数据类型。由于元组与数组之间存在很多共性,因此 TypeScript 使用数组来表示元组。在 TypeScript 中,元组类型是数组类型的子类型。元组是长度固定的数组,并且元组中每个元素都有确定的类型。

元组的定义

定义元组类型的语法与定义数组字面量的语法相似,具体语法如下所示:

[T0, T1, ..., Tn]

该语法中的 T0T1Tn 表示元组中元素的类型,针对元组中每一个位置上的元素都需要定义其数据类型。

下例中,我们使用元组来表示二维坐标系中的一个点。该元组中包含两个 number 类型的元素,分别表示点的横坐标和纵坐标。示例如下:

const point: [number, number] = [0, 0];

元组中每个元素的类型不必相同。例如,可以定义一个表示考试成绩的元组,元组的第一个元素是 string 类型的科目名,第二个元素是 number 类型的分数。示例如下:

const score: [string, number] = ['math', 100];

元组的值实际上是一个数组,在给元组类型赋值时,数组中每个元素的类型都要与元组类型的定义保持兼容。例如,对于 [number, number] 类型的元组,它只接受包含两个 number 类型元素的数组。示例如下:

const point: [number, number] = [0, 0];

若数组元素的类型与元组类型的定义不匹配,则会产生编译错误。示例如下:

let point: [number, number];

point = [0, 'y'];    // 编译错误
point = ['x', 0];    // 编译错误
point = ['x', 'y'];  // 编译错误

在给元组类型赋值时,还要保证数组中元素的数量与元组类型定义中元素的数量保持一致,否则将产生编译错误。示例如下:

let point: [number, number];

point = [0];        // 编译错误
point = [0, 0, 0];  // 编译错误

只读元组

元组可以定义为只读元组,这与只读数组是类似的。只读元组类型是只读数组类型的子类型。定义只读元组有以下两种方式:

  • 使用 readonly 修饰符。

  • 使用 Readonly<T> 工具类型。

以上两种定义只读元组的方式只是语法不同,它们在功能上没有任何差别。

readonly

TypeScript 3.4 版本中引入了一种新语法,使用 readonly 修饰符能够定义只读元组。在定义只读元组时,将 readonly 修饰符置于元组类型之前即可。示例如下:

const point: readonly [number, number] = [0, 0];

此例中,point 是包含两个元素的只读元组。

Readonly<T>

由于 TypeScript 3.4 支持了使用 readonly 修饰符来定义只读元组,所以从 TypeScript 3.4 开始可以使用 Readonly<T> 工具类型来定义只读元组。示例如下:

const point: Readonly<[number, number]> = [0, 0];

此例中,point 是包含两个元素的只读元组。在 Readonly<T> 类型中,类型参数 T 的值为元组类型 [number, number]

注意事项

在给只读元组类型赋值时,允许将常规元组类型赋值给只读元组类型,但是不允许将只读元组类型赋值给常规元组类型。换句话说,不能通过赋值操作来放宽对只读元组的约束。示例如下:

const a: [number] = [0];
const ra: readonly [number] = [0];

const x: readonly [number] = a; // 正确

const y: [number] = ra;         // 编译错误

访问元组中的元素

由于元组在本质上是数组,所以我们可以使用访问数组元素的方法去访问元组中的元素。在访问元组中指定位置上的元素时,编译器能够推断出相应的元素类型。示例如下:

const score: [string, number] = ['math', 100];

const course = score[0];   // string
const grade = score[1];    // number

const foo: boolean = score[0];
//    ~~~
//    编译错误!类型 'string' 不能赋值给类型 'boolean'

const bar: boolean = score[1];
//    ~~~
//    编译错误!类型 'number' 不能赋值给类型 'boolean'

当访问数组中不存在的元素时不会产生编译错误。与之不同的是,当访问元组中不存在的元素时会产生编译错误。示例如下:

const score: [string, number] = ['math', 100];

const foo = score[2];
//          ~~~~~~~~
//          编译错误!该元组类型只有两个元素,找不到索引为'2'的元素

修改元组元素值的方法与修改数组元素值的方法相同。示例如下:

const point: [number, number] = [0, 0];

point[0] = 1;
point[1] = 1;

元组类型中的可选元素

在定义元组时,可以将某些元素定义为可选元素。定义元组可选元素的语法是在元素类型之后添加一个问号 “?”,具体语法如下所示:

[T0?, T1?, ..., Tn?]

该语法中的 T0T1Tn 表示元组中元素的类型。

如果元组中同时存在可选元素和必选元素,那么可选元素必须位于必选元素之后,具体语法如下所示:

[T0, T1?, ..., Tn?]

该语法中的 T0 表示必选元素的类型,T1Tn 表示可选元素的类型。

下例中定义了一个包含三个元素的元组 tuple,其中第一个元素是必选元素,后两个元素是可选元素:

const tuple: [boolean, string?, number?] = [true, 'yes', 1];

在给元组赋值时,可以不给元组的可选元素赋值。例如,对于上例中的 tuple 元组,它的值可以为仅包含一个元素的数组,或者是包含两个元素的数组,再或者是包含三个元素的数组。示例如下:

let tuple: [boolean, string?, number?] = [true, 'yes', 1];

tuple = [true];
tuple = [true, 'yes'];
tuple = [true, 'yes', 1];

元组类型中的剩余元素

在定义元组类型时,可以将最后一个元素定义为剩余元素。定义元组剩余元素类型的语法如下所示:

[...T[]]

该语法中,元组的剩余元素是数组类型,T 表示剩余元素的类型。

下例中,在元组 tuple 的定义中包含了剩余元素。其中,元组的第一个元素为 number 类型,其余的元素均为 string 类型。示例如下:

const tuple: [number, ...string[]] = [0, 'a', 'b'];

如果元组类型的定义中含有剩余元素,那么该元组的元素数量是开放的,它可以包含零个或多个指定类型的剩余元素。示例如下:

let tuple: [number, ...string[]];

tuple = [0];
tuple = [0, 'a'];
tuple = [0, 'a', 'b'];
tuple = [0, 'a', 'b', 'c'];

元组的长度

对于经典的元组类型,即不包含可选元素和剩余元素的元组而言,元组中元素的数量是固定的。也就是说,元组拥有一个固定的长度。TypeScript 编译器能够识别出元组的长度并充分利用该信息来进行类型检查。示例如下:

function f(point: [number, number]) {
    // 编译器推断出length的类型为数字字面量类型2
    const length = point.length; // 3

    if (length === 3) {   // 5    // 编译错误!条件表达式永远为 false
        // ...
    }
}

此例第 3 行,TypeScript 编译器能够推断出常量 length 的类型为数字字面量类型 2。第 5 行在 if 条件表达式中,数字字面量类型 2 与数字字面量类型 3 没有交集。因此,编译器能够分析出该比较结果永远为 false。在这种情况下,编译器将产生编译错误。

当元组中包含了可选元素时,元组的长度不再是一个固定值。编译器能够根据元组可选元素的数量识别出元组所有可能的长度,进而构造出一个由数字字面量类型构成的联合类型来表示元组的长度。示例如下:

const tuple: [boolean, string?, number?] = [true, 'yes', 1];

let len = tuple.length;      // 1 | 2 | 3

len = 1;
len = 2;
len = 3;

len = 4;                     // 编译错误!类型'4'不能赋值给类型'1 | 2 | 3'

此例第 1 行,元组 tuple 中共包含 3 个元素,其中第一个元素是必选元素,后面两个元素是可选元素,元组 tuple 中可能的元素数量为 1、2 或 3 个。TypeScript 编译器能够推断出此信息并构造出联合类型 “1 | 2 | 3” 作为该元组 length 属性的类型。

第 5、6、7 行,允许将数字 1、2 和 3 赋值给 len 变量。第 9 行,不允许将数字 4 赋值给 len 变量,因为数字字面量类型 4 与联合类型 “1 | 2 | 3” 不兼容。

若元组类型中定义了剩余元素,那么该元组拥有不定数量的元素。因此,该元组 length 属性的类型将放宽为 number 类型。示例如下:

const tuple: [number, ...string[]] = [0, 'a'];

const len = tuple.length; // number

元组类型与数组类型的兼容性

前文提到过,元组类型是数组类型的子类型,只读元组类型是只读数组类型的子类型。在进行赋值操作时,允许将元组类型赋值给类型兼容的元组类型和数组类型。示例如下:

const point: [number, number] = [0, 0];

const nums: number[] = point; // 正确

const strs: string[] = point; // 编译错误

此例中,元组 point 的两个元素都是 number 类型,因此允许将 point 赋值给 number 数组类型,而不允许将 point 赋值给 string 数组类型。

元组类型允许赋值给常规数组类型和只读数组类型,但只读元组类型只允许赋值给只读数组类型。示例如下:

const t: [number, number] = [0, 0];
const rt: readonly [number, number] = [0, 0];

let a: number[] = t;

let ra: readonly number[];
ra = t;
ra = rt;

由于数组类型是元组类型的父类型,因此不允许将数组类型赋值给元组类型。示例如下:

const nums: number[] = [0, 0];

let point: [number, number] = nums;
//  ~~~~~
//  编译错误