深入理解 TypeScript
请支持本书:购买捐赠
(广告,请勿屏蔽。)

19 数组类型



在本章中,我们将研究如何在 TypeScript 中对数组进行类型定义。

19.1 数组的角色

数组在 JavaScript 中可以扮演以下角色(单个角色或多个角色混合):

TypeScript 通过提供多种数组类型定义方式来适应这两种角色。接下来我们将逐一介绍。

19.2 数组的类型定义方式

19.2.1 数组角色“列表”:数组类型字面量 vs. 接口类型 Array

数组类型字面量由元素类型后跟 [] 组成。在下面的代码中,数组类型字面量是 string[]

// Each Array element has the type `string`:
const myStringArray: string[] = ['fee', 'fi', 'fo', 'fum'];

数组类型字面量是使用全局泛型接口类型 Array 的简写形式

const myStringArray: Array<string> = ['fee', 'fi', 'fo', 'fum'];

如果元素类型更复杂,我们需要为数组类型字面量添加括号

(number|string)[]
(() => boolean)[]

在这种情况下,泛型类型 Array 更适用

Array<number|string>
Array<() => boolean>

19.2.2 数组角色“元组”:元组类型字面量

如果数组具有固定长度,并且每个元素都有不同的、固定的类型,具体取决于其位置,则可以使用元组类型字面量,例如 [string, string, boolean]

const yes: [string, string, boolean] = ['oui', 'sí', true];

19.2.3 类似数组的对象:具有索引签名的接口

如果一个接口只有一个索引签名,我们可以将其用于数组

interface StringArray {
  [index: number]: string;
}
const strArr: StringArray = ['Huey', 'Dewey', 'Louie'];

同时具有索引签名和属性签名的接口只能用于对象(因为索引元素和属性需要同时定义)

interface FirstNamesAndLastName {
  [index: number]: string;
  lastName: string;
}

const ducks: FirstNamesAndLastName = {
  0: 'Huey',
  1: 'Dewey',
  2: 'Louie',
  lastName: 'Duck',
};

19.3 陷阱:类型推断并不总是能正确识别数组类型

19.3.1 推断数组类型很困难

由于数组的两种角色,TypeScript 不可能总是猜到正确的类型。例如,考虑以下分配给变量 fields 的数组字面量

const fields: Fields = [
  ['first', 'string', true],
  ['last', 'string', true],
  ['age', 'number', false],
];

fields 的最佳类型是什么?以下都是合理的选择

type Fields = Array<[string, string, boolean]>;
type Fields = Array<[string, ('string'|'number'), boolean]>;
type Fields = Array<Array<string|boolean>>;
type Fields = [
  [string, string, boolean],
  [string, string, boolean],
  [string, string, boolean],
];
type Fields = [
  [string, 'string', boolean],
  [string, 'string', boolean],
  [string, 'number', boolean],
];
type Fields = [
  Array<string|boolean>,
  Array<string|boolean>,
  Array<string|boolean>,
];

19.3.2 非空数组字面量的类型推断

当我们使用非空数组字面量时,TypeScript 默认推断列表类型(而不是元组类型)

// %inferred-type: (string | number)[]
const arr = [123, 'abc'];

唉,这并不总是我们想要的

function func(p: [number, number]) {
  return p;
}
// %inferred-type: number[]
const pair1 = [1, 2];

// @ts-expect-error: Argument of type 'number[]' is not assignable to
// parameter of type '[number, number]'. [...]
func(pair1);

我们可以通过向 const 声明添加类型注释来解决这个问题,这可以避免类型推断

const pair2: [number, number] = [1, 2];
func(pair2); // OK

19.3.3 空数组字面量的类型推断

如果我们使用空数组字面量初始化一个变量,则 TypeScript 最初会推断出类型 any[],并随着我们进行更改而逐步更新该类型

// %inferred-type: any[]
const arr1 = [];

arr1.push(123);
// %inferred-type: number[]
arr1;

arr1.push('abc');
// %inferred-type: (string | number)[]
arr1;

请注意,最初推断的类型不受以后发生的事情的影响。

如果我们使用赋值而不是 .push(),则工作原理相同

// %inferred-type: any[]
const arr1 = [];

arr1[0] = 123;
// %inferred-type: number[]
arr1;

arr1[1] = 'abc';
// %inferred-type: (string | number)[]
arr1;

相反,如果数组字面量至少有一个元素,则元素类型是固定的,以后不会更改

// %inferred-type: number[]
const arr = [123];

// @ts-expect-error: Argument of type '"abc"' is not assignable to
// parameter of type 'number'. (2345)
arr.push('abc');

19.3.4 数组的 const 断言和类型推断

我们可以在数组字面量后面加上 const 断言

// %inferred-type: readonly ["igneous", "metamorphic", "sedimentary"]
const rockCategories =
  ['igneous', 'metamorphic', 'sedimentary'] as const;

我们声明 rockCategories 不会改变。这会产生以下影响

以下是更多带有和不带有 const 断言的数组字面量示例

// %inferred-type: readonly [1, 2, 3, 4]
const numbers1 = [1, 2, 3, 4] as const;
// %inferred-type: number[]
const numbers2 = [1, 2, 3, 4];

// %inferred-type: readonly [true, "abc"]
const booleanAndString1 = [true, 'abc'] as const;
// %inferred-type: (string | boolean)[]
const booleanAndString2 = [true, 'abc'];
19.3.4.1 const 断言的潜在陷阱

const 断言有两个潜在的陷阱。

首先,推断的类型尽可能窄。这会导致 let 声明的变量出现问题:我们不能分配除用于初始化的元组之外的任何其他元组

let arr = [1, 2] as const;

arr = [1, 2]; // OK

// @ts-expect-error: Type '3' is not assignable to type '2'. (2322)
arr = [1, 3];

其次,无法修改通过 as const 声明的元组

let arr = [1, 2] as const;

// @ts-expect-error: Cannot assign to '1' because it is a read-only
// property. (2540)
arr[1] = 3;

这既不是优点也不是缺点,但我们需要意识到这一点。

19.4 陷阱:TypeScript 假设索引永远不会越界

每当我们通过索引访问数组元素时,TypeScript 总是假设索引在范围内(A 行)

const messages: string[] = ['Hello'];

// %inferred-type: string
const message = messages[3]; // (A)

由于这个假设,message 的类型是 string。而不是我们可能预期的 undefinedundefined|string

如果我们使用元组类型,我们会收到错误消息

const messages: [string] = ['Hello'];

// @ts-expect-error: Tuple type '[string]' of length '1' has no element
// at index '1'. (2493)
const message = messages[1];

as const 也会产生相同的效果,因为它会导致推断出元组类型。