likes
comments
collection
share

TypeScript手册——对象类型 (Object Types)

作者站长头像
站长
· 阅读数 1

免责声明: 本文为翻译自 Typescript 官方手册内容,非原创。版权归原作者所有。

原文来源: Object Types

翻译者注: 本人在翻译过程中已尽最大努力确保内容的准确性和完整性,但翻译工作难免有疏漏。如读者在阅读中发现任何问题,欢迎提出建议或直接查阅原文以获取最准确的信息。

在 JavaScript 中,我们组织和传递数据的基本方式是通过对象。在 TypeScript 中,我们通过对象类型来表示这些。

正如我们所看到的,它们可以是匿名的:

function greet(person: { name: string; age: number }) {
  return "Hello " + person.name;
}

或者他们可以通过使用接口来命名:

interface Person {
  name: string;
  age: number;
}
 
function greet(person: Person) {
  return "Hello " + person.name;
}

或类型别名:

type Person = {
  name: string;
  age: number;
};
 
function greet(person: Person) {
  return "Hello " + person.name;
}

在上述所有三个示例中,我们编写了函数,这些函数接收包含属性 name(必须是 string)和 age(必须是 number)的对象。

快速参考

我们为类型和接口都提供了速查表,如果你想一眼就看到重要的日常语法,可以快速查阅。

属性修饰符

对象类型中的每个属性可以指定几件事:类型、属性是否可选、是否可以写入该属性。

可选属性

很多时候,我们会发现自己在处理可能设有属性的对象。在这些情况下,我们可以通过在它们的名称末尾添加问号(?)来将这些属性标记为可选。

interface PaintOptions {
  shape: Shape;
  xPos?: number;
  yPos?: number;
}
 
function paintShape(opts: PaintOptions) {
  // ...
}
 
const shape = getShape();

paintShape({ shape });
paintShape({ shape, xPos: 100 });
paintShape({ shape, yPos: 100 });
paintShape({ shape, xPos: 100, yPos: 100 });

在这个例子中,xPosyPos 都被视为可选的。我们可以选择提供其中任何一个,所以以上对 paintShape 的每次调用都是有效的。所有的可选性实际上只是说,如果属性被设置了,它最好有一个特定的类型。

我们也可以从这些属性中读取 - 但是当我们在 strictNullChecks 下进行操作时,TypeScript 会告诉我们它们可能是 undefined

function paintShape(opts: PaintOptions) {
  let xPos = opts.xPos;
                   // (property) PaintOptions.xPos?: number | undefined
  let yPos = opts.yPos;
                   // (property) PaintOptions.yPos?: number | undefined
  // ...
}

在 JavaScript 中,即使属性从未被设置,我们仍然可以访问它 - 它只会给我们一个 undefined 的值。我们可以通过检查它来特殊处理 undefined

function paintShape(opts: PaintOptions) {
  let xPos = opts.xPos === undefined ? 0 : opts.xPos;
       // let xPos: number
  let yPos = opts.yPos === undefined ? 0 : opts.yPos;
       // let yPos: number
  // ...
}

请注意,对未指定值设置默认值的这种模式在 JavaScript 中非常常见,因此 JavaScript 提供了语法来支持它。

function paintShape({ shape, xPos = 0, yPos = 0 }: PaintOptions) {
  console.log("x coordinate at", xPos);
                                  // (parameter) xPos: number
  console.log("y coordinate at", yPos);
                                  // (parameter) yPos: number
  // ...
}

我们在 paintShape 的参数中使用了解构模式,并为 xPosyPos 提供了默认值。现在,xPosyPos 都肯定存在于 paintShape 的主体内,但对于任何调用 paintShape 的方法来说都是可选的。

请注意,目前还没有办法在解构模式中放置类型注释。这是因为在 JavaScript 中,以下语法已经有了不同的含义。

function draw({ shape: Shape, xPos: number = 100 /*...*/ }) {
  render(shape);
  // 找不到名称“shape”。你是否指的是“Shape”?
  render(xPos);
  // 找不到名称“xPos”。
}

在对象解构模式中,shape: Shape 表示“获取属性 shape 并将其在本地重新定义为一个名为 Shape 的变量。同样,xPos: number 创建了一个名为 number 的变量,其值基于参数的 xPos

readonly 属性

属性也可以在 TypeScript 中标记为只读(readonly)。虽然这不会改变运行时的任何行为,但在类型检查期间,标记为 readonly 的属性不能被写入。

interface SomeType {
  readonly prop: string;
}
 
function doSomething(obj: SomeType) {
  // 我们可以读取'obj.prop'.
  console.log(`prop has the value '${obj.prop}'.`);
 
  // 但是我们不能给它重新赋值。
  obj.prop = "hello";
  // 报错:无法为“prop”赋值,因为它是只读属性。
}

使用 readonly 修饰符并不一定意味着一个值完全不可变 - 或者换句话说,它的内部内容不能被改变。这只是意味着属性本身不能被重写。

interface Home {
  readonly resident: { name: string; age: number };
}
 
function visitForBirthday(home: Home) {
  // 我们可以读取和更新 'home.resident' 属性。
  console.log(`Happy birthday ${home.resident.name}!`);
  home.resident.age++;
}
 
function evict(home: Home) {
  // 但是我们不能直接在 'Home' 上写入 'resident' 属性。
  home.resident = {
    // 报错:无法为“resident”赋值,因为它是只读属性。
    name: "Victor the Evictor",
    age: 42,
  };
}

管理对 readonly 含义的期望是很重要的。在 TypeScript 的开发过程中,标明对象应如何使用的意图是很有用的。当检查两种类型是否兼容时,TypeScript 并不考虑这两种类型上的属性是否为 readonly,因此通过别名也可以更改 readonly 属性。

interface Person {
  name: string;
  age: number;
}
 
interface ReadonlyPerson {
  readonly name: string;
  readonly age: number;
}
 
let writablePerson: Person = {
  name: "Person McPersonface",
  age: 42,
};
 
// 有效
let readonlyPerson: ReadonlyPerson = writablePerson;
 
console.log(readonlyPerson.age); // '42'
writablePerson.age++;
console.log(readonlyPerson.age); // '43'

使用映射修饰符,你可以删除 readonly 属性。

索引签名

有时候你可能没有办法提前知道类型的所有属性名称,但是你知道值的结构。

在这种情况下,你可以使用索引签名来描述可能的值的类型,例如:

interface StringArray {
  [index: number]: string;
}
 
const myArray: StringArray = getStringArray();
const secondItem = myArray[1];
          // const secondItem: string

在上面,我们有一个带有索引签名的 StringArray 接口。这个索引签名声明当用 number 索引 StringArray 时,它将返回一个 string

只有部分类型被允许用于索引签名属性:stringnumbersymbol、模板字符串模式以及仅由这些组成的联合类型。

虽然字符串索引签名是描述"字典"模式的强大方式,但它们也强制所有属性都符合其返回类型。这是因为字符串索引声明了 obj.property 也可以作为 obj["property"] 来访问。在以下示例中,name 的类型与字符串索引的类型不匹配,类型检查器将会抛出一个错误:

interface NumberDictionary {
  [index: string]: number;
 
  length: number; // ok
  name: string;
  // 报错:类型“string”的属性“name”不能赋给“string”索引类型“number”
}

然而,如果索引签名是属性类型的联合,则可以接受不同类型的属性:

interface NumberOrStringDictionary {
  [index: string]: number | string;
  length: number; // ok, length is a number
  name: string; // ok, name is a string
}

最后,你可以将索引签名设置为 readonly,以防止对其索引进行赋值:

interface ReadonlyStringArray {
  readonly [index: number]: string;
}
 
let myArray: ReadonlyStringArray = getReadOnlyStringArray();
myArray[2] = "Mallory";
// 报错:类型“ReadonlyStringArray”中的索引签名仅允许读取。

你不能设置 myArray[2],因为索引签名是 readonly

额外属性检查

对象被赋予类型的地方和方式在类型系统中会产生一定的影响。其中一个关键的例子就是额外属性检查,它在创建和分配到对象类型时更彻底地验证了对象。

interface SquareConfig {
  color?: string;
  width?: number;
}
 
function createSquare(config: SquareConfig): { color: string; area: number } {
  return {
    color: config.color || "red",
    area: config.width ? config.width * config.width : 20,
  };
}
 
let mySquare = createSquare({ colour: "red", width: 100 });
// 报错:类型“{ colour: string; width: number; }”的参数不能赋给类型“SquareConfig”的参数。
//  对象字面量只能指定已知的属性,但“colour”中不存在类型“SquareConfig”。是否要写入 color?

注意给 createSquare 的参数被拼写为 colour 而不是 color。在普通的 JavaScript 中,这种情况会默默地失败。

你可能会争辩说这个程序类型正确,因为 width 属性是兼容的,没有 color 属性存在,并且额外的 colour 属性并不重要。

然而,TypeScript 持有的立场是这段代码可能存在错误。当将对象字面量在赋值给其他变量或作为参数传递时会得到特殊处理,并进行额外属性检查。如果一个对象字面量有任何“目标类型”没有的属性,你就会得到一个错误警告:

let mySquare = createSquare({ colour: "red", width: 100 });
// 类型“{ colour: string; width: number; }”的参数不能赋给类型“SquareConfig”的参数。
//  对象字面量只能指定已知的属性,但“colour”中不存在类型“SquareConfig”。是否要写入 color?

绕过这些检查实际上非常简单。最简单的方法就是直接使用类型断言:

let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);

然而,更好的方法可能是添加一个字符串索引签名,如果你确定对象可以有一些在某种特殊方式下使用的额外属性。如果SquareConfig可以具备上述类型的color和width属性,但也可以有任何数量的其他属性,那么我们可以这样定义它:

interface SquareConfig {
  color?: string;
  width?: number;
  [propName: string]: any;
}

我们在这里说的是,SquareConfig 可以有任意数量的属性,只要它们不是 colorwidth,它们的类型就无关紧要。

最后一种规避这些检查的方法,可能会让人感到有些惊讶,那就是将对象赋值给另一个变量:由于 squareOptions 的赋值不会经过额外属性的检查,因此编译器不会报错。

let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);

上述解决方法只有在 squareOptionsSquareConfig 之间有共同的属性时才会工作。在这个例子中,它是属性 width。但是,如果变量没有任何公共对象属性,这种方法就会失败。例如:

let squareOptions = { colour: 'red' }
let mySquare = createSquare(squareOptions)
// 类型“{ colour: string; }”与类型“SquareConfig”不具有相同的属性。

请记住,对于如上所示的简单代码,你可能不应该试图“避开”这些检查。对于有方法和保存状态的更复杂的对象字面量,你可能需要记住这些技巧,但是大多数的额外属性错误实际上都是 bug。

这意味着,如果你在处理类似于选项包的额外属性检查问题时,你可能需要修改一些类型声明。在这种情况下,如果可以传递一个同时具有 colorcolour 属性的对象给 createSquare,你应该修改 SquareConfig 的定义以反映这一点。

扩展类型

拥有可能是其他类型更具体版本的类型是非常常见的。例如,我们可能有一个 BasicAddress 类型,它描述了在美国发送信件和包裹所必需的字段。

interface BasicAddress {
  name?: string;
  street: string;
  city: string;
  country: string;
  postalCode: string;
}

在某些情况下,这就足够了,但是如果该地址的建筑物有多个单元,通常会有一个与之关联的单元号。然后,我们可以描述一个带单元的地址(AddressWithUnit)。

interface AddressWithUnit {
  name?: string;
  unit: string;
  street: string;
  city: string;
  country: string;
  postalCode: string;
}

这样能解决问题,但是缺点在于我们不得不重复所有来自 BasicAddress 的其他字段,而我们的修改纯粹是添加性的。相反,我们可以扩展原始的 BasicAddress 类型,只添加对 AddressWithUnit 来说独有的新字段。

interface BasicAddress {
  name?: string;
  street: string;
  city: string;
  country: string;
  postalCode: string;
}
 
interface AddressWithUnit extends BasicAddress {
  unit: string;
}

interfaceextends 关键字允许我们有效地从其他命名类型复制成员,并添加我们想要的任何新成员。这对于减少我们必须编写的类型声明代码很有用,并且能够表明多个不同声明的相同属性可能是相关的。例如,AddressWithUnit 不需要重复 street 属性,因为 street 源自 BasicAddress,读者会知道这两种类型以某种方式是相关的。

interface 也可以从多种类型扩展。

interface Colorful {
  color: string;
}
 
interface Circle {
  radius: number;
}
 
interface ColorfulCircle extends Colorful, Circle {}
 
const cc: ColorfulCircle = {
  color: "red",
  radius: 42,
};

交叉类型

接口使我们能够通过扩展它们从其他类型构建新的类型。 TypeScript 提供了另一种名为交叉类型的结构,主要用于组合现有的对象类型。

交叉类型是使用 & 运算符定义的。

interface Colorful {
  color: string;
}

interface Circle {
  radius: number;
}
 
type ColorfulCircle = Colorful & Circle;

在这里,我们将 ColorfulCircle 进行了交叉,产生了一个具有 ColorfulCircle 的所有成员的新类型。

function draw(circle: Colorful & Circle) {
  console.log(`Color was ${circle.color}`);
  console.log(`Radius was ${circle.radius}`);
}
 
// okay
draw({ color: "blue", radius: 42 });
 
// oops
draw({ color: "red", raidus: 42 });
// 类型“{ color: string; raidus: number; }”的参数不能赋给类型“Colorful & Circle”的参数。
//   对象字面量只能指定已知的属性,但“raidus”中不存在类型“Colorful & Circle”。是否要写入 radius?

接口 vs 交叉类型

我们刚刚研究了两种组合类型的方法,它们看起来相似,但实际上又有细微的区别。使用接口时,我们可以通过 extends 子句从其他类型继承,并且我们可以通过交叉类型做类似的事情,并且用类型别名命名结果。两者之间的主要区别在于如何处理冲突,这种差异通常是你在接口和交叉类型的类型别名之间选择其一的主要原因。

泛型对象类型

让我们想象一个 Box 类型,它可以包含任何值 - stringnumberGiraffe,等等。

interface Box {
  contents: any;
}

现在,contents 属性被定义为 any 类型,虽然这可以运行,但可能会导致后续的意外。

我们也可以使用 unknown,但这意味着在我们已知 contents 类型的情况下,我们需要进行预防性检查,或者使用容易出错的类型断言。

interface Box {
  contents: unknown;
}
 
let x: Box = {
  contents: "hello world",
};
 
// 我们可以检查 'x.contents'
if (typeof x.contents === "string") {
  console.log(x.contents.toLowerCase());
}
 
// 或者我们可以使用类型断言
console.log((x.contents as string).toLowerCase());

一种类型安全的方法是为每种类型的 contents 搭建不同的 Box 类型。

interface NumberBox {
  contents: number;
}
 
interface StringBox {
  contents: string;
}
 
interface BooleanBox {
  contents: boolean;
}

但这意味着我们将不得不创建不同的函数,或者对这些类型进行函数的重载。

function setContents(box: StringBox, newContents: string): void;
function setContents(box: NumberBox, newContents: number): void;
function setContents(box: BooleanBox, newContents: boolean): void;
function setContents(box: { contents: any }, newContents: any) {
  box.contents = newContents;
}

这里有大量的样板代码。而且,我们可能稍后需要引入新的类型和重载。这是令人沮丧的,因为我们的盒子类型和重载实际上都是相同的。

相反,我们可以创建一个泛型的 Box 类型,它声明了一个类型参数。

interface Box<Type> {
  contents: Type;
}

你可以将其理解为:“一个 Type 类型的 Box 是其 contents 属性具有 Type 类型的东西”。后面当我们提到 Box 时,我们必须在 Type 的位置提供一个类型参数。

let box: Box<string>;

Box 视为真实类型的模板,其中 Type 是一个占位符,将被替换为其他类型。当 TypeScript 看到 Box 时,它会将 Box 中的每个 Type 实例都替换为 string,最终得到像 { contents: string } 这样的东西。换句话说,Box<string> 和我们之前的 StringBox 的工作方式是相同的。

interface Box<Type> {
  contents: Type;
}

interface StringBox {
  contents: string;
}
 
let boxA: Box<string> = { contents: "hello" };
boxA.contents;
        // (property) Box<string>.contents: string
 
let boxB: StringBox = { contents: "world" };
boxB.contents;
        // (property) StringBox.contents: string

Box 具有可重用性,因为 Type 可以替换为任何东西。这意味着当我们需要一个新类型的盒子时,我们不需要声明一个新的 Box 类型(虽然如果我们愿意的话,我们当然可以声明)。

interface Box<Type> {
  contents: Type;
}
 
interface Apple {
  // ....
}
 
// 和 '{ contents: Apple }' 一样
type AppleBox = Box<Apple>;

这也意味着我们可以通过使用泛型函数来完全避免重载。

function setContents<Type>(box: Box<Type>, newContents: Type) {
  box.contents = newContents;
}

值得注意的是,类型别名也可以是泛型。我们本可以按以下方式定义我们新的 Box<Type> 接口:

interface Box<Type> {
  contents: Type;
}

通过使用类型别名来代替:

type Box<Type> = {
  contents: Type;
};

由于类型别名不同于接口,它可以描述的不仅仅是对象类型,我们也可以使用它们来编写其他种类的泛型辅助类型。

type OrNull<Type> = Type | null;
 
type OneOrMany<Type> = Type | Type[];
 
type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;
           
type OneOrManyOrNull<Type> = OneOrMany<Type> | null
 
type OneOrManyOrNullStrings = OneOrManyOrNull<string>;
               
type OneOrManyOrNullStrings = OneOrMany<string> | null

我们稍后会回到类型别名的话题上来。

数组(Array)类型

泛型对象类型通常是某种容器类型,它们独立于其包含的元素类型工作。数据结构以这种方式工作是理想的,因为它们可以在不同的数据类型之间重复使用。

原来我们在整个手册中一直在使用这样的类型:Array 类型。每当我们写出像 number[]string[] 这样的类型时,其实就是 Array<number>Array<string> 的简写。

function doSomething(value: Array<string>) {
  // ...
}
 
let myArray: string[] = ["hello", "world"];
 
// 这两种都可以
doSomething(myArray);
doSomething(new Array("hello", "world"));

就像上面的 Box 类型一样,Array 本身也是一个泛型类型。

interface Array<Type> {
  /**
   * 获取或设置数组的长度
   */
  length: number;
 
  /**
   * 从数组中移除最后一个元素并返回它
   */
  pop(): Type | undefined;
 
  /**
   * 将新元素添加到数组中,并返回数组的新长度
   */
  push(...items: Type[]): number;
 
  // ...
}

现代 JavaScript 也提供了其他的泛型数据结构,如 Map<K, V>, Set<T>, 和 Promise<T>。所有这些实际上意味着,由于 MapSetPromise 的行为方式,它们可以与任何类型的集合一起工作。

只读数组(ReadonlyArray)类型

ReadonlyArray 是一种特殊类型,用于描述不应被更改的数组。

function doStuff(values: ReadonlyArray<string>) {
  // 我们可以读取 'values'...
  const copy = values.slice();
  console.log(`The first value is ${values[0]}`);
 
  // 但是我们不能修改 'values'.
  values.push("hello!");
  // 类型“readonly string[]”上不存在属性“push”。
}

就像属性的 readonly 修饰符一样,它主要是我们可以用于表示意图的工具。当我们看到一个返回 ReadonlyArrays 的函数时,它告诉我们我们不打算更改其内容,当我们看到一个消耗 ReadonlyArrays 的函数时,它告诉我们我们可以将任何数组传递给该函数而无需担心它会更改其内容。

Array 不同,我们不能使用 ReadonlyArray 构造函数。

new ReadonlyArray("red", "green", "blue");
// 报错:“ReadonlyArray”仅表示类型,但在此处却作为值使用。

相反,我们可以将常规 Arrays 分配给 ReadonlyArrays

const roArray: ReadonlyArray<string> = ["red", "green", "blue"];

就像 TypeScript 为 Array<Type> 提供了 Type[] 的简写语法,它也为 ReadonlyArray<Type> 提供了 readonly Type[] 的简写语法。

function doStuff(values: readonly string[]) {
  // 我们可以读取 'values'
  const copy = values.slice();
  console.log(`The first value is ${values[0]}`);
 
  // 但我们不能修改 'values'.
  values.push("hello!");
  // 报错:类型“readonly string[]”上不存在属性“push”。
}

需要注意的最后一件事是,普通数组和只读数组之间的可赋值性并不是双向的,这与 readonly 属性修饰符不同。

let x: readonly string[] = [];
let y: string[] = [];
 
x = y;
y = x;
// 报错:类型 "readonly string[]" 为 "readonly",不能分配给可变类型 "string[]"。

元组类型

元组类型是另一种 Array 类型,它准确地知道自己包含多少个元素,并且在特定位置上精确地知道其包含的类型。

type StringNumberPair = [string, number];

在这里,StringNumberPairstringnumber 的元组类型。就像 ReadonlyArray 一样,它在运行时没有表示形式,但对于 TypeScript 来说非常重要。对于类型系统来说,StringNumberPair 描述了其 0 索引包含 string1 索引包含 number 的数组。

function doSomething(pair: [string, number]) {
  const a = pair[0];
       // const a: string
  const b = pair[1];
       // const b: number
  // ...
}
 
doSomething(["hello", 42]);

如果我们尝试索引超过元素数量的位置,我们将会收到一个错误。

function doSomething(pair: [string, number]) {
  // ...
 
  const c = pair[2];
  // 报错:长度为 "2" 的元组类型 "[string, number]" 在索引 "2" 处没有元素。
}

我们也可以使用JavaScript的数组解构来解构元组

function doSomething(stringHash: [string, number]) {
  const [inputString, hash] = stringHash;
 
  console.log(inputString);
                  // const inputString: string
 
  console.log(hash);
               // const hash: number
}

元组类型在严重依赖约定的 API 中非常有用,其中每个元素的含义都是“显而易见”的。这给了我们在解构它们时为变量命名的灵活性。在上述例子中,我们可以将元素 0 和 1 命名为任何我们想要的。

然而,由于并非所有用户对什么是显而易见都持有相同的观点,因此可能值得重新考虑是否使用带有描述性属性名称的对象可能更适合你的 API。

除了这些长度检查之外,这样的简单元组类型等同于声明特定索引属性的数组版本的类型,并且用数字文字类型声明 length

interface StringNumberPair {
  // 专有属性
  length: 2;
  0: string;
  1: number;
 
  // 其他 'Array<string | number>' 成员...
  slice(start?: number, end?: number): Array<string | number>;
}

你可能会对此感兴趣的另一件事是,元组可以通过在元素类型后写一个问号(?)来拥有可选属性。可选的元组元素只能放在最后,并且还会影响 length 类型。

type Either2dOr3d = [number, number, number?];
 
function setCoordinate(coord: Either2dOr3d) {
  const [x, y, z] = coord;
              // const z: number | undefined
 
  console.log(`Provided coordinates had ${coord.length} dimensions`);
                                                  // (property) length: 2 | 3
}

元组也可以有剩余元素,这些元素必须是数组/元组类型。

type StringNumberBooleans = [string, number, ...boolean[]];
type StringBooleansNumber = [string, ...boolean[], number];
type BooleansStringNumber = [...boolean[], string, number];
  • StringNumberBooleans 描述的是一个元组,其前两个元素分别是 stringnumber,但随后可能有任意数量的 boolean
  • StringBooleansNumber 描述的是一个元组,其第一个元素是 string,然后是任意数量的 boolean,最后是一个 number
  • BooleansStringNumber 描述的是一个元组,其开始元素是任意数量的 boolean,最后是一个 string 后跟一个 number

带有剩余元素的元组没有设定的“长度” - 它只有一组在不同位置上的已知元素。

const a: StringNumberBooleans = ["hello", 1];
const b: StringNumberBooleans = ["beautiful", 2, true];
const c: StringNumberBooleans = ["world", 3, true, false, true, false, true];

为什么可选元素和剩余元素可能有用呢?好吧,它允许 TypeScript 将元组与参数列表相对应。元组类型可以在剩余形参和实参中使用,因此以下情况:

function readButtonInput(...args: [string, number, ...boolean[]]) {
  const [name, version, ...input] = args;
  // ...
}

基本等同于:

function readButtonInput(name: string, version: number, ...input: boolean[]) {
  // ...
}

当你想要用剩余形参获取可变数量的实参,并且你需要最少数量的元素,但是你不想引入中间变量时,这就很方便了。

只读(readonly)元组类型

关于元组类型的最后一点说明 - 元组类型有 readonly 变体,可以通过在它们前面加上 readonly 修饰符来指定 - 就像数组简写语法一样。

function doSomething(pair: readonly [string, number]) {
  // ...
}

正如你可能预期的,TypeScript 中不允许写入只读元组的任何属性。

function doSomething(pair: readonly [string, number]) {
  pair[0] = "hello!";
  // 报错:无法为“0”赋值,因为它是只读属性。
}

元组在大多数代码中往往被创建并保持不变,因此尽可能将类型注释为 readonly 元组是一个好的默认选择。考虑到带有 const 断言的数组字面量会被推断为 readonly 元组类型,这一点也非常重要。

let point = [3, 4] as const;
 
function distanceFromOrigin([x, y]: [number, number]) {
  return Math.sqrt(x ** 2 + y ** 2);
}
 
distanceFromOrigin(point);
// 类型“readonly [3, 4]”的参数不能赋给类型“[number, number]”的参数。
//   类型 "readonly [3, 4]" 为 "readonly",不能分配给可变类型 "[number, number]"。

在这里,distanceFromOrigin 从不修改其元素,但却期望一个可变的元组。由于 point 的类型被推断为只读的 [3, 4],它将与 [number, number] 不兼容,因为后者的类型无法保证 point 的元素不会被改变。