数组和元组
一个灵活的,一个固定的
选择你的冒险
JavaScript 数组非常灵活,可以在其中保存任何混合的值:
const elements = [true, null, undefined, 42];
elements.push("even", ["more"]);
// Value of elements: [true, null, undefined, 42, "even", ["more"]]
在大多数情况下,JavaScript 数组通常只用于保存特定类型的值。添加不同类型的值可能会让读者感到困惑,更糟糕的是,这可能导致程序中出现错误的结果。
TypeScript 遵循最佳实践,即通过记住数组初始时包含的数据类型,只允许数组对该类型的数据进行操作。
在这个示例中,TypeScript 知道 warriors
数组最初包含 string
类型化值,因此虽然允许添加更多 string
类型化值,但不允许添加任何其他类型的数据:
const warriors = ["Artemisia", "Boudica"];
// Ok: "Zenobia" 是一个字符串
warriors.push("Zenobia");
warriors.push(true);
// ~~~~
// 类型“boolean”的参数不能赋给类型“string”的参数。
你可以认为 TypeScript 从数组的初始成员推断数组类型,类似于它如何从变量的初始值理解变量类型。TypeScript 通常尝试从值如何分配来理解代码的预期类型,数组也不例外。
数组类型
与其他变量声明一样,用于存储数组的变量不需要具有初始值。变量可以从 undefined
开始,稍后接收数组值。
TypeScript 希望你通过为变量提供类型注解来让它知道数组中要包含哪些类型的值。数组的类型注解需要数组中的元素类型之后添加[]
:
let arrayOfNumbers: number[];
arrayOfNumbers = [4, 8, 15, 16, 23, 42];
数组类型也可以用类似 Array<number>
的语法书写,这称为类泛型。大多数开发人员更喜欢更简单的number[]。类在第 8 章 “类”中介绍,泛型在第 10 章 “泛型”中介绍。
数组和函数类型
数组类型是语法容器的一个示例,其中函数类型可能需要括号来区分函数类型中的内容。括号可用于表示注解的哪一部分是函数返回值,哪部分是周围的数组类型。
这里的 createStrings
类型(函数类型)与 stringCreators
不同,后者是数组类型:
// 类型是一个返回字符串数组的函数
let createStrings: () => string[];
// 类型是一个函数数组,每个函数都返回一个字符串
let stringCreators: (() => string)[];
联合类型数组
可以使用联合类型来表示数组的每个元素可以是多个选择类型之一。
将数组类型与联合一起使用时,可能需要使用括号来表示注解的哪一部分是数组的内容或周围的联合类型。在数组联合类型中使用括号很重要——以下两种类型并不相同:
// 类型是一个数字或字符串数组
let stringOrArrayOfNumbers: string | number[];
// 类型是一个数组,数组中的元素是数字或字符串
let arrayOfStringOrNumbers: (string | number)[];
如果 TypeScript 包含多种类型的元素,则从数组的声明中理解它是一个联合类型的数组。换句话说,数组元素的类型是数组中元素的所有可能类型的联合。
这里,namesMaybe
是 (string | undefined)[]
,因为它同时具有 string
值和 undefined
值:
// 类型是 (string | undefined)[]
const namesMaybe = [
"Aqualtune",
"Blenda",
undefined,
];
演化 Any 数组
如果你在初始时没有为一个空数组的变量加上类型注解,TypeScript 将会将该数组视为演化 any[]
,意味着它可以接受任意类型的内容。与演化 any
变量一样,我们不喜欢演化 any[]
数组。它们部分地削弱了 TypeScript 类型检查的好处,因为它允许你添加潜在不正确的值。
这个 values
数组开始时包含 any
元素,然后演变为包含 string
元素,再演变为包含 number | string
元素:
// Type: any[]
let values = [];
// Type: string[]
values.push('');
// Type: (number | string)[]
values[0] = 0;
与变量一样,允许数组演变 any
类型——并且通常使用 any
类型——部分削弱了 TypeScript 类型检查的目的。TypeScript 知道你的值应该是什么类型时,它的表现最佳。
多维数组
一个 2D 数组或数组的数组将有两个“[]”:
let arrayOfArraysOfNumbers: number[][];
arrayOfArraysOfNumbers = [
[1, 2, 3],
[2, 4, 6],
[3, 6, 9],
];
一个 3D 数组或数组的数组将有三个“[]”。4D 数组有四个“[]”。5D 数组有五个“[]”。你可以猜到 6D 数组及以后的发展趋势。
这些多维数组类型不会向数组类型引入任何新概念。将 2D 数组视为接收基本类型,它恰好在末尾有 []
,并在它后面添加一个 []
。
这个 arrayOfArraysOfNumbers
数组的类型为 number[][]
,也可以用 (number[])[]
表示:
// 类型:number[][]
let arrayOfArraysOfNumbers: (number[])[];
数组成员
TypeScript 了解典型的基于索引的访问,用于检索数组的成员以返回该数组类型的元素。
这个 defenders
数组的类型为 string[]
,因此 defender
是 string
:
const defenders = ["Clarenza", "Dina"];
// 类型:string
const defender = defenders[0];
联合成员的类型数组本身就是该联合类型。
这里,soldiersOrDates 的类型是 (string | Date)[],因此 soldierOrDate 变量的类型是 string | Date:
const soldiersOrDates = ["Deborah Sampson", new Date(1782, 6, 3)];
// 类型:Date | string
const soldierOrDate = soldiersOrDates[0];
警告:不安全的成员
众所周知,TypeScript 类型系统在技术上是不安全的:它可以获得基本正确的类型,但有时它对值类型的理解可能不正确。特别要指出的是,数组是类型系统中不安全的来源。默认情况下,TypeScript 假定所有数组成员访问都返回该数组的成员,即使在 JavaScript 中,访问索引大于数组长度的数组元素会给出undefined
。
以下代码在默认的 TypeScript 编译器设置不会报错:
function withElements(elements: string[]) {
console.log(elements[9001].length); // 没有类型错误
}
withElements(["It's", "over"]);
作为读者,我们可以推断它会在运行时崩溃,“无法读取未定义属性 'length'”,但 TypeScript 故意不确保检索到的数组成员存在。它将上面代码片段中的 elements[9001]
看作是 string
,而不是 undefined
。
TypeScript 确实有一个 –noUncheckedIndexedAccess
标志,它使数组查找更加严格和类型安全,但它非常严格,大多数项目都不使用它。本书不会涉及它。第 13 章 “配置选项”链接到资源,可以深入解释 TypeScript 的所有配置选项。
展开运算符和剩余参数
还记得第 5 章“函数”中函数的 …
剩余参数吗?剩余参数和数组扩展,都使用 …
运算符,是与 JavaScript 中的数组交互的关键方式。TypeScript 都能理解它们。
展开运算符
可以使用 …
展开运算符将数组连接在一起。TypeScript 认为结果数组包含的值可以来自任意一个输入数组。
如果输入数组的类型相同,输出数组的类型也将相同。如果将两个不同类型的数组放在一起创建一个新数组,则新数组将被理解为一个联合类型数组,其中的元素是两种基本类型中的任何一种。
在这里,已知 conjoined
数组同时包含 string
类型的值和 number
类型的值,因此它的类型被推断为 (string | number)[]
:
// 类型:string[]
const soldiers = ["Harriet Tubman", "Joan of Arc", "Khutulun"];
// 类型:number[]
const soldierAges = [90, 19, 45];
// 类型:(string | number)[]
const conjoined = [...soldiers, ...soldierAges];
扩展剩余参数
TypeScript 识别并将对 JavaScript 实践中的 …
扩展数组作为剩余参数进行类型检查。用作剩余参数的数组必须与剩余参数具有相同的数组类型。
下面的 logWarriors
函数的 …names
剩余参数只接受 string
值。允许扩展类型为 string[]
的数组,但不允许展开 number[]
的数组:
function logWarriors(greeting: string, ...names: string[]) {
for (const name of names) {
console.log(`${greeting}, ${name}!`);
}
}
const warriors = ["Cathay Williams", "Lozen", "Nzinga"];
logWarriors("Hello", ...warriors);
const birthYears = [1844, 1840, 1583];
logWarriors("Born in", ...birthYears);
// ~~~~~~~~~~~~~
// 错误:类型“number”的参数不能赋给类型“string”的参数。
元组
尽管 JavaScript 数组在理论上可以是任意大小,但有时使用固定大小的数组(也称为 tuple)很有用。元组数组在每个索引处都有一个特定的已知类型,该类型可能比数组中所有可能成员的联合类型更具体。声明元组类型的语法看起来像数组字面量,但使用类型代替元素值。
以下代码中,数组 yearAndWarrior
声明为元组类型,索引 0 处为 number
,索引 1 处为 string
:
let yearAndWarrior: [number, string];
yearAndWarrior = [530, "Tomyris"]; // Ok
yearAndWarrior = [false, "Tomyris"];
// ~~~~~
// 错误:不能将类型“boolean”分配给类型“number”。
yearAndWarrior = [530];
// 错误:不能将类型“[number]”分配给类型“[number, string]”。
// 源具有 1 个元素,但目标需要 2 个。
元组通常在 JavaScript 中与数组解构一起使用,以便能够一次分配多个值,例如根据单个条件设置两个变量的初始值。
例如,TypeScript 在这里认识到 year
总会是一个 number
,而 warrior
总会是一个 string
:
// year 类型:number
// warrior 类型: string
let [year, warrior] = Math.random() > 0.5
? [340, "Archidamia"]
: [1828, "Rani of Jhansi"];
元组可分配性
TypeScript 将元组类型视为比可变长度数组类型更具体。这意味着可变长度数组类型不能分配给元组类型。
这里,尽管我们作为人类可能会将 pairLoose
视为具有 [boolean, number]
内容,但 TypeScript 推断它是更一般的 (boolean | number)[]
类型:
// 类型 (boolean | number)[]
const pairLoose = [false, 123];
const pairTupleLoose: [boolean, number] = pairLoose;
// ~~~~~~~~~~~~~~
// 错误:不能将类型“(number | boolean)[]”分配给类型“[boolean, number]”。
// 目标仅允许 2 个元素,但源中的元素可能不够。ts(2322)
如果 pairLoose
本身已声明为 boolean | number
,则允许将其值分配给 pairTuple
。
不同长度的元组也不能相互赋值,因为 TypeScript 在元组类型中包含了知道元组中有多少成员的信息。
在这里,tupleTwoExtra
必须有两个成员,因此尽管 tupleThree
以正确的成员开头,但它的第三个成员阻止将其分配给 tupleTwoExtra
:
const tupleThree: [boolean, number, string] = [false, 1583, "Nzinga"];
const tupleTwoExact: [boolean, number] = [tupleThree[0], tupleThree[1]];
const tupleTwoExtra: [boolean, number] = tupleThree;
// ~~~~~~~~~~~~~
// 错误:不能将类型“[boolean, number, string]”分配给类型“[boolean, number]”。
// 源具有 3 个元素,但目标仅允许 2 个。
元组作为剩余参数
由于元组被视为具有长度和元素类型的更具体类型信息的数组,因此它们对于存储要传递给函数的参数特别有用。TypeScript 能够为作为 …
剩余参数传递的元组提供准确的类型检查。
这里,logPair
函数的参数类型为 string
和 number
。尝试传入类型为 (string | number)[]
的值作为参数不会是安全的类型,因为内容可能不匹配:它们可能是相同的类型,或者错误顺序的一种类型。但是,如果 TypeScript 知道值为 [string, number]
元组,它就会理解这些值是匹配的:
function logPair(name: string, value: number) {
console.log(`${name} has ${value}`);
}
const pairArray = ["Amage", 1];
logPair(...pairArray);
// 错误:扩张参数必须具有元组类型或传递给 rest 参数。
const pairTupleIncorrect: [number, string] = [1, "Amage"];
logPair(...pairTupleIncorrect);
// 错误:类型“number”的参数不能赋给类型“string”的参数。
const pairTupleCorrect: [string, number] = ["Amage", 1];
logPair(...pairTupleCorrect); // Ok
如果你真的大胆地使用剩余参数元组,你可以将它们与数组混合使用,以存储多个函数调用的参数列表。这里,trios
是一个元组数组,每个元组的第二个成员也是一个元组。trios.forEach(trio => logTrio(…trio))
被认为是安全的,因为每个...trio
的参数类型都与 logTrio
的参数类型相匹配。然而,trios.forEach(logTrio)
是不可分配的,因为它试图将整个 [string, [number, boolean]
作为第一个参数传递,即类型为 string
:
function logTrio(name: string, value: [number, boolean]) {
console.log(`${name} has ${value[0]} (${value[1]}`);
}
const trios: [string, [number, boolean]][] = [
["Amanitore", [1, true]],
["Æthelflæd", [2, false]],
["Ann E. Dunwoody", [3, false]]
];
trios.forEach(trio => logTrio(...trio)); // Ok
trios.forEach(logTrio);
// ~~~~~~~
// 类型“(name: string, value: [number, boolean]) => void”的参数不能
// 赋给类型“(value: [string, [number, boolean]], index: number, array: [string, [number, boolean]][]) => void”的参数。
// 参数“name”和“value” 的类型不兼容。
// 不能将类型“[string, [number, boolean]]”分配给类型“string”。
元组推断
TypeScript 通常将创建的数组视为可变长度数组,而不是元组。如果它看到一个数组被用作变量的初始值或函数的返回值,那么它将假定一个灵活大小的数组,而不是一个固定大小的元组。
以下 firstCharAndSize
函数被推断为返回 (string | number)[]
,而不是 [string, number]
,因为这是为其返回的数组字面量推断的类型:
// 返回类型:(string | number)[]
function firstCharAndSize(input: string) {
return [input[0], input.length];
}
// firstChar 类型:string | number
// size 类型:string | number
const [firstChar, size] = firstCharAndSize("Gudit");
TypeScript 中有两种常见的方式来表明一个值应该是更具体的元组类型而不是一般的数组类型:显式元组类型和 const
断言。
显式元组类型
元组类型可以在类型注解中使用,例如函数的返回类型注解。如果函数被声明为返回元组类型,并且返回一个数组字面量,那么这个数组字面量将被推断为元组类型,而不是更一般的可变长度数组。
此 firstCharAndSizeExplicit
函数版明确声明它返回 string
和 number
的元组:
// 返回类型:[string, number]
function firstCharAndSizeExplicit(input: string): [string, number] {
return [input[0], input.length];
}
// firstChar 类型:string
// size 类型:number
const [firstChar, size] = firstCharAndSizeExplicit("Cathay Williams");
Const 断言元组
对于显式类型注解,手动输入元组类型会像任何显式类型注解一样很麻烦。这会增加你需要编写和更新的额外语法。
作为一种替代方法,TypeScript 提供了一个 as const 操作符,称为 const 断言,可以放在值之后。const 断言告诉 TypeScript 在推断其类型时使用最字面化、只读的形式。如果放在数组字面量后面,则表示该数组应该被当作元组处理:
// Type: (string | number)[]
const unionArray = [1157, "Tomoe"];
// Type: readonly [1157, "Tomoe"]
const readonlyTuple = [1157, "Tomoe"] as const;
请注意,as const
断言不仅仅是从灵活大小的数组切换到固定大小的元组:它们还向 TypeScript 表示元组是只读的,不能在期望允许它修改值的地方使用。
在此示例中,允许修改 pairMutable
,因为它具有传统的显式元组类型。但 as const
使值不可分配给可变的 pairAlsoMutable
,并且不允许修改常量 pairConst
的成员:
const pairMutable: [number, string] = [1157, "Tomoe"];
pairMutable[0] = 1247; // Ok
const pairAlsoMutable: [number, string] = [1157, "Tomoe"] as const;
// ~~~~~~~~~~~~~~~
// 错误:类型 "readonly [1157, "Tomoe"]" 为 "readonly",不能分配给可变类型 "[number, string]"
const pairConst = [1157, "Tomoe"] as const;
pairConst[0] = 1247;
// ~
// 错误:无法为“0”赋值,因为它是只读属性。
实际上,只读元组对于函数返回很方便。从返回元组的函数返回的值通常会立即解构,因此只读元组不会妨碍使用该函数。
这个 firstCharAndSizeAsConst
返回一个 readonly [string, number]
,但使用代码只关心从该元组中检索值:
// 返回类型:readonly [string, number]
function firstCharAndSizeAsConst(input: string) {
return [input[0], input.length] as const;
}
// firstChar 类型:string
// size 类型:number
const [firstChar, size] = firstCharAndSizeAsConst("Ching Shih");
只读对象和 const 断言在第 9 章 “类型修饰符”中进行了更深入的介绍。
总结
在本章中,你了解了如何声明数组和检索其成员:
- 使用
[]
声明数组类型 - 使用括号声明函数或联合类型的数组
- TypeScript 如何将数组元素理解为数组的类型
- 使用
…
展开运算符 和剩余参数 - 声明元组类型以表示固定大小的数组
- 使用类型注解或
as const
断言创建元组
*现在你已经读完了这一章,你最好练习一下学到的东西 https://learningtypescript.com/arrays。
海盗最喜欢的数据结构是什么?
啊哈哈哈-数组!
评论 (0)