Skip to content

TypeScript 的数组类型

JavaScript 数组在 TypeScript 里面分成两种类型,分别是数组(array)和元组(tuple)。

数组

TypeScript 数组有一个根本特征:所有成员的类型必须相同,但是数量不确定,可以是无限数量的成员,也可以是零成员。

数组的类型有两种写法。 第一种写法是在数组成员类型+方括号

ts
// [] 写法
let arr: number[] = [1, 2, 3];
let arr: (number | string)[];
// 泛型写法
let arr: Array<number> = [1, 2, 3];
let arr: Array<number | string>;

正是由于成员数量可以动态变化,所以 TypeScript 不会对数组边界进行检查,越界访问数组并不会报错

TypeScript 允许使用方括号读取数组成员的类型,注意这是类型运算。

ts
type Names = string[];
type Name = Names[0]; // string

数组的类型推断

如果数组变量没有声明类型,TypeScript 就会推断数组成员的类型。这时,推断行为会因为值的不同,而有所不同。

如果变量的初始值是空数组,那会推断数组类型是any[]

ts
const arr = []; // 推断为 any[]

后面,为这个数组赋值时,TypeScript 会自动更新类型推断。

ts
const arr = [];
arr; // 推断为 any[]

arr.push(123);
arr; // 推断类型为 number[]

arr.push("abc");
arr; // 推断类型为 (string|number)[]

上面示例中,数组变量arr的初始值是空数组,然后随着新成员的加入,TypeScript 会自动修改推断的数组类型。

但是,类型推断的自动更新只发生初始值为空数组的情况。如果初始值不是空数组,类型推断就不会更新。

只读数组,const 断言

TypeScript 允许声明只读数组,方法是在数组类型前面加上readonly关键字。

ts
const arr: readonly number[] = [0, 1];

arr[1] = 2; // 报错
arr.push(3); // 报错
delete arr[0]; // 报错

注意,readonly关键字不能与数组的泛型写法一起使用, TypeScript 提供了两个专门的泛型,用来生成只读数组的类型。

ts
const arr: readonly Array<number> = [0, 1]; // 报错
const a1: ReadonlyArray<number> = [0, 1]; // ok
const a2: Readonly<number[]> = [0, 1]; // ok

多维数组

TypeScript 使用T[][]的形式,表示二维数组,T是最底层数组成员的类型。

ts
var multi: number[][] = [
  [1, 2, 3],
  [23, 24, 25],
];

上面示例中,变量multi的类型是number[][],表示它是一个二维数组,最底层的数组成员类型是number

元组

元组(tuple)是 TypeScript 特有的数据类型,JavaScript 没有单独区分这种类型。它表示成员类型可以自由设置的数组,即数组的各个成员的类型可以不同。

元组必须明确声明每个成员的类型。

ts
const s: [string, string, boolean] = ["a", "b", true];

使用元组时,必须明确给出类型声明(上例的[number]),不能省略,否则 TypeScript 会把一个值自动推断为数组。

元组成员的类型可以添加问号后缀(?),表示该成员是可选的,问号只能用于元组的尾部成员。

ts
let a: [number, number?] = [1];

由于需要声明每个成员的类型,所以大多数情况下,元组的成员数量是有限的,从类型声明就可以明确知道,元组包含多少个成员,越界的成员会报错。

但是,使用扩展运算符(...),可以表示不限成员数量的元组。

ts
type NamedNums = [string, ...number[]];

const a: NamedNums = ["A", 1, 2];
const b: NamedNums = ["B", 1, 2, 3];

上面示例中,元组类型NamedNums的第一个成员是字符串,后面的成员使用扩展运算符来展开一个数组,从而实现了不定数量的成员。

扩展运算符用在元组的任意位置都可以,但是它后面只能是数组或元组。

ts
type t1 = [string, number, ...boolean[]];
type t2 = [string, ...boolean[], number];
type t3 = [...boolean[], string, number];

如果不确定元组成员的类型和数量,可以写成下面这样。

由于元组的成员都是数值索引,即索引类型都是number,所以可以像下面这样读取。

ts
type Tuple = [string, number, Date];
type TupleEl = Tuple[number]; // string|number|Date

只读元组

元组也可以是只读的,不允许修改,有两种写法。

ts
// 写法一
type t = readonly [number, string];

// 写法二
type t = Readonly<[number, string]>;

跟数组一样,只读元组是元组的父类型。所以,元组可以替代只读元组,而只读元组不能替代元组。

成员数量的推断

如果没有可选成员和扩展运算符,TypeScript 会推断出元组长度。

ts
function f(point: [number, number]) {
  if (point.length === 3) {
    // 报错
    // ...
  }
}

如果使用了扩展运算符,TypeScript 就无法推断出成员数量。

ts
const myTuple: [...string[]] = ["a", "b", "c"];

if (myTuple.length === 4) {
  // 正确
  // ...
}

一旦扩展运算符使得元组的成员数量无法推断,TypeScript 内部就会把该元组当成数组处理。

扩展运算符与成员数量

扩展运算符(...)将数组转换成一个逗号分隔的序列,这时 TypeScript 会认为这个序列的成员数量是不确定的,因为数组的成员数量是不确定的。

这导致如果函数调用时,使用扩展运算符传入函数参数,可能发生参数数量与数组长度不匹配的报错。

ts
const arr = [1, 2];

function add(x: number, y: number) {
}

add(...arr); // 报错

上面示例会报错,原因是函数add()只能接受两个参数,但是传入的是...arrTypeScript 认为转换后的参数个数是不确定的

有些函数可以接受任意数量的参数,这时使用扩展运算符就不会报错。

解决这个问题的一个方法,就是把成员数量不确定的数组,写成成员数量确定的元组,再使用扩展运算符。

ts
const arr: [number, number] = [1, 2];

function add(x: number, y: number) {
  // ...
}

add(...arr); // 正确

上面示例中,arr是一个拥有两个成员的元组,所以 TypeScript 能够确定...arr可以匹配函数add()的参数数量,就不会报错了。