对象类型

在 JavaScript 中,我们分组和传递数据的基本方式是通过对象。在 TypeScript 中,我们通过对象类型 (object types) 来表示它们。

如我们所见,它们可以是匿名的

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

也可以通过使用 interface (接口)

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

或类型别名 (type alias) 来命名

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

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

快速参考

我们提供了 typeinterface 的速查表,如果您想快速浏览日常常用的语法,可以参考这些表格。

属性修饰符

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

可选属性

很多时候,我们会处理一些属性可能未设置的对象。在这种情况下,我们可以通过在属性名后面添加问号 (?) 将这些属性标记为可选的

ts
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 });
Try

在这个示例中,xPosyPos 都被视为可选。我们可以选择提供它们中的任何一个,因此上面对 paintShape 的每次调用都是有效的。可选性真正说明的是:如果该属性确实被设置了,它最好具有指定的类型。

我们也可以读取这些属性——但在 strictNullChecks 开启的情况下,TypeScript 会告诉我们它们可能为 undefined

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

在 JavaScript 中,即使属性从未被设置,我们仍然可以访问它——它只会返回 undefined 值。我们可以通过检查 undefined 来专门处理它。

ts
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
// ...
}
Try

请注意,这种为未指定值设置默认值的模式非常常见,以至于 JavaScript 提供了支持它的语法。

ts
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
// ...
}
Try

在这里,我们为 paintShape 的参数使用了解构模式,并为 xPosyPos 提供了默认值。现在,xPosyPospaintShape 的函数体内确定存在,但对于任何 paintShape 的调用者来说它们依然是可选的。

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

ts
function draw({ shape: Shape, xPos: number = 100 /*...*/ }) {
render(shape);
Cannot find name 'shape'. Did you mean 'Shape'?2552Cannot find name 'shape'. Did you mean 'Shape'?
render(xPos);
Cannot find name 'xPos'.2304Cannot find name 'xPos'.
}
Try

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

readonly 属性

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

ts
interface SomeType {
readonly prop: string;
}
 
function doSomething(obj: SomeType) {
// We can read from 'obj.prop'.
console.log(`prop has the value '${obj.prop}'.`);
 
// But we can't re-assign it.
obj.prop = "hello";
Cannot assign to 'prop' because it is a read-only property.2540Cannot assign to 'prop' because it is a read-only property.
}
Try

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

ts
interface Home {
readonly resident: { name: string; age: number };
}
 
function visitForBirthday(home: Home) {
// We can read and update properties from 'home.resident'.
console.log(`Happy birthday ${home.resident.name}!`);
home.resident.age++;
}
 
function evict(home: Home) {
// But we can't write to the 'resident' property itself on a 'Home'.
home.resident = {
Cannot assign to 'resident' because it is a read-only property.2540Cannot assign to 'resident' because it is a read-only property.
name: "Victor the Evictor",
age: 42,
};
}
Try

管理对 readonly 所隐含意义的预期非常重要。在开发过程中,它对于向 TypeScript 表示对象应如何使用非常有用。当检查两个类型是否兼容时,TypeScript 不会考虑这些类型的属性是否为 readonly,因此 readonly 属性也可以通过别名发生改变。

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

使用映射修饰符 (mapping modifiers),你可以移除 readonly 属性。

索引签名 (Index Signatures)

有时你无法提前知道类型所有属性的名称,但你却知道值的形状。

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

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

在上面,我们有一个包含索引签名的 StringArray 接口。这个索引签名规定:当 StringArraynumber 索引时,它将返回一个 string

索引签名属性仅允许使用以下类型:stringnumbersymbol、模板字符串模式,以及由这些类型组成的联合类型。

支持多种类型的索引器是可能的...

支持多种类型的索引器是可能的。请注意,当同时使用 numberstring 索引器时,从数字索引器返回的类型必须是字符串索引器返回类型的子类型。这是因为当使用 number 进行索引时,JavaScript 实际上会在索引对象之前将其转换为 string。这意味着使用 100(一个 number)索引与使用 "100"(一个 string)索引是一样的,因此两者必须保持一致。

ts
interface Animal {
name: string;
}
 
interface Dog extends Animal {
breed: string;
}
 
// Error: indexing with a numeric string might get you a completely separate type of Animal!
interface NotOkay {
[x: number]: Animal;
'number' index type 'Animal' is not assignable to 'string' index type 'Dog'.2413'number' index type 'Animal' is not assignable to 'string' index type 'Dog'.
[x: string]: Dog;
}
Try

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

ts
interface NumberDictionary {
[index: string]: number;
 
length: number; // ok
name: string;
Property 'name' of type 'string' is not assignable to 'string' index type 'number'.2411Property 'name' of type 'string' is not assignable to 'string' index type 'number'.
}
Try

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

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

最后,你可以使索引签名 readonly 以防止对其索引进行赋值

ts
interface ReadonlyStringArray {
readonly [index: number]: string;
}
 
let myArray: ReadonlyStringArray = getReadOnlyStringArray();
myArray[2] = "Mallory";
Index signature in type 'ReadonlyStringArray' only permits reading.2542Index signature in type 'ReadonlyStringArray' only permits reading.
Try

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

多余属性检查 (Excess Property Checks)

对象被赋值给类型的位置和方式会在类型系统中产生差异。其中一个关键例子是多余属性检查,它会在对象创建并赋值给对象类型时,更彻底地校验该对象。

ts
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 });
Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?2561Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?
Try

注意传给 createSquare 的参数拼写成了 colour 而不是 color。在纯 JavaScript 中,这种情况会静默失败。

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

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

ts
let mySquare = createSquare({ colour: "red", width: 100 });
Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?2561Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?
Try

绕过这些检查实际上非常简单。最简单的方法是使用类型断言 (type assertion)

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

然而,如果你确定对象可以包含一些以特殊方式使用的额外属性,更好的方法是添加一个字符串索引签名。如果 SquareConfig 可以拥有上述类型的 colorwidth 属性,但也可以拥有任意数量的其他属性,那么我们可以这样定义它

ts
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: unknown;
}
Try

这里我们说明 SquareConfig 可以拥有任意数量的属性,只要它们不是 colorwidth,它们的类型就不重要。

绕过这些检查的最后一种方法(可能会让人有些惊讶)是将对象赋值给另一个变量:由于赋值给 squareOptions 不会触发多余属性检查,编译器不会报错

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

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

ts
let squareOptions = { colour: "red" };
let mySquare = createSquare(squareOptions);
Type '{ colour: string; }' has no properties in common with type 'SquareConfig'.2559Type '{ colour: string; }' has no properties in common with type 'SquareConfig'.
Try

请记住,对于像上面那样的简单代码,你可能不应该试图“绕过”这些检查。对于具有方法和持有状态的更复杂的对象字面量,你可能需要记住这些技巧,但大多数多余属性错误实际上都是 bug。

这意味着如果你在处理类似选项对象(option bags)时遇到了多余属性检查问题,你可能需要修改一些类型定义。在这种情况下,如果向 createSquare 传递一个同时具有 colorcolour 属性的对象是可以的,那么你应该修复 SquareConfig 的定义来反映这一点。

扩展类型 (Extending Types)

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

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

在某些情况下这已经足够了,但如果地址所在的建筑物有多个单元,地址通常会关联一个单元编号。那么我们可以描述一个 AddressWithUnit

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

这样做完成了任务,但缺点是我们不得不重复 BasicAddress 中的所有其他字段,而我们的更改纯粹是累加性的。相反,我们可以扩展原始的 BasicAddress 类型,并只添加 AddressWithUnit 所特有的新字段。

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

interface 上的 extends 关键字允许我们有效地从其他命名类型复制成员,并添加我们想要的任何新成员。这对于减少我们需要编写的类型声明样板代码,以及表明某些相同属性的不同声明之间可能存在关联的意图非常有用。例如,AddressWithUnit 不需要重复 street 属性,而且由于 street 源自 BasicAddress,读者会知道这两个类型在某种程度上是相关的。

interface 也可以扩展自多个类型。

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

交叉类型 (Intersection Types)

interface 允许我们通过扩展来从其他类型构建新类型。TypeScript 提供了另一种称为交叉类型的构造,主要用于组合现有的对象类型。

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

ts
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
 
type ColorfulCircle = Colorful & Circle;
Try

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

ts
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 });
Object literal may only specify known properties, but 'raidus' does not exist in type 'Colorful & Circle'. Did you mean to write 'radius'?2561Object literal may only specify known properties, but 'raidus' does not exist in type 'Colorful & Circle'. Did you mean to write 'radius'?
Try

接口扩展 vs. 交叉类型

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

如果同名接口被定义,TypeScript 将尝试在属性兼容的情况下合并它们。如果属性不兼容(即它们具有相同的属性名但类型不同),TypeScript 将抛出错误。

在交叉类型的情况下,具有不同类型的属性将自动合并。当稍后使用该类型时,TypeScript 会期望该属性同时满足两种类型,这可能会产生意想不到的结果。

例如,以下代码会抛出错误,因为属性是不兼容的

ts
interface Person {
name: string;
}
interface Person {
name: number;
}

相比之下,以下代码可以编译,但会导致 never 类型

ts
interface Person1 {
name: string;
}
 
interface Person2 {
name: number;
}
 
type Staff = Person1 & Person2
 
declare const staffer: Staff;
staffer.name;
(property) name: never
Try

在这种情况下,Staff 需要 name 属性既是字符串又是数字,这导致属性的类型为 never

泛型对象类型 (Generic Object Types)

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

ts
interface Box {
contents: any;
}
Try

目前,contents 属性被类型化为 any,这虽然有效,但可能导致以后的事故。

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

ts
interface Box {
contents: unknown;
}
 
let x: Box = {
contents: "hello world",
};
 
// we could check 'x.contents'
if (typeof x.contents === "string") {
console.log(x.contents.toLowerCase());
}
 
// or we could use a type assertion
console.log((x.contents as string).toLowerCase());
Try

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

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

但这意味着我们必须创建不同的函数或函数重载来操作这些类型。

ts
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;
}
Try

那会有大量的样板代码。此外,我们以后可能需要引入新的类型和重载。这很令人沮丧,因为我们的盒子类型和重载实际上都是一样的。

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

ts
interface Box<Type> {
contents: Type;
}
Try

你可以将其理解为“Box of Type 是其 contents 具有 Type 类型的东西”。稍后,当我们引用 Box 时,必须在 Type 的位置提供一个类型实参

ts
let box: Box<string>;
Try

Box 看作是一个真实类型的模板,其中 Type 是一个占位符,它将被其他类型替换。当 TypeScript 看到 Box<string> 时,它会将 Box<Type> 中的每个 Type 实例替换为 string,并最终处理类似于 { contents: string } 的东西。换句话说,Box<string> 和我们之前的 StringBox 的作用是一样的。

ts
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
Try

Box 是可重用的,因为 Type 可以被任何东西替代。这意味着当我们为新类型需要一个盒子时,根本不需要声明一个新的 Box 类型(尽管如果愿意,我们当然可以这样做)。

ts
interface Box<Type> {
contents: Type;
}
 
interface Apple {
// ....
}
 
// Same as '{ contents: Apple }'.
type AppleBox = Box<Apple>;
Try

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

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

值得注意的是,类型别名也可以是泛型的。我们可以定义我们新的 Box<Type> 接口,即

ts
interface Box<Type> {
contents: Type;
}
Try

通过使用类型别名来代替

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

由于类型别名不像接口那样只能描述对象类型,我们也可以用它们来编写其他类型的泛型辅助类型。

ts
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
Try

我们稍后会回到类型别名。

Array 类型

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

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

ts
function doSomething(value: Array<string>) {
// ...
}
 
let myArray: string[] = ["hello", "world"];
 
// either of these work!
doSomething(myArray);
doSomething(new Array("hello", "world"));
Try

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

ts
interface Array<Type> {
/**
* Gets or sets the length of the array.
*/
length: number;
 
/**
* Removes the last element from an array and returns it.
*/
pop(): Type | undefined;
 
/**
* Appends new elements to an array, and returns the new length of the array.
*/
push(...items: Type[]): number;
 
// ...
}
Try

现代 JavaScript 还提供了其他通用的数据结构,如 Map<K, V>Set<T>Promise<T>。这一切真正意味着,由于 MapSetPromise 的行为方式,它们可以与任何类型的集合一起工作。

ReadonlyArray 类型

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

ts
function doStuff(values: ReadonlyArray<string>) {
// We can read from 'values'...
const copy = values.slice();
console.log(`The first value is ${values[0]}`);
 
// ...but we can't mutate 'values'.
values.push("hello!");
Property 'push' does not exist on type 'readonly string[]'.2339Property 'push' does not exist on type 'readonly string[]'.
}
Try

就像属性的 readonly 修饰符一样,它主要是一个我们可以用来表达意图的工具。当我们看到一个返回 ReadonlyArray 的函数时,它告诉我们内容是不允许更改的;而当我们看到一个消费 ReadonlyArray 的函数时,它告诉我们,我们可以将任何数组传递给该函数,而不必担心它会改变其内容。

Array 不同,没有我们可以使用的 ReadonlyArray 构造函数。

ts
new ReadonlyArray("red", "green", "blue");
'ReadonlyArray' only refers to a type, but is being used as a value here.2693'ReadonlyArray' only refers to a type, but is being used as a value here.
Try

相反,我们可以将常规的 Array 分配给 ReadonlyArray

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

正如 TypeScript 为 Array<Type> 提供了 Type[] 的简写语法一样,它也为 ReadonlyArray<Type> 提供了 readonly Type[] 的简写语法。

ts
function doStuff(values: readonly string[]) {
// We can read from 'values'...
const copy = values.slice();
console.log(`The first value is ${values[0]}`);
 
// ...but we can't mutate 'values'.
values.push("hello!");
Property 'push' does not exist on type 'readonly string[]'.2339Property 'push' does not exist on type 'readonly string[]'.
}
Try

最后要注意的一点是,与 readonly 属性修饰符不同,常规 ArrayReadonlyArray 之间的可分配性不是双向的。

ts
let x: readonly string[] = [];
let y: string[] = [];
 
x = y;
y = x;
The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.4104The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.
Try

元组类型 (Tuple Types)

元组类型 是另一种 Array 类型,它准确知道它包含多少个元素,以及在特定位置包含哪些类型。

ts
type StringNumberPair = [string, number];
Try

这里,StringNumberPair 是一个由 stringnumber 组成的元组类型。像 ReadonlyArray 一样,它在运行时没有表示,但对 TypeScript 意义重大。对于类型系统,StringNumberPair 描述了那些索引 0 包含 string 且索引 1 包含 number 的数组。

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

如果我们尝试在超过元素数量的地方索引,我们将会得到一个错误。

ts
function doSomething(pair: [string, number]) {
// ...
 
const c = pair[2];
Tuple type '[string, number]' of length '2' has no element at index '2'.2493Tuple type '[string, number]' of length '2' has no element at index '2'.
}
Try

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

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

元组类型在高度基于约定的 API 中非常有用,其中每个元素的含义都很“明显”。这赋予了我们在解构变量时命名任何我们想要的变量的灵活性。在上面的示例中,我们能够将元素 01 命名为任何我们想要的名字。

然而,由于并非每个用户对“明显”的看法都相同,可能值得重新考虑使用带有描述性属性名称的对象是否对你的 API 更好。

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

ts
interface StringNumberPair {
// specialized properties
length: 2;
0: string;
1: number;
 
// Other 'Array<string | number>' members...
slice(start?: number, end?: number): Array<string | number>;
}
Try

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

ts
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
}
Try

元组也可以有剩余元素 (rest elements),它们必须是数组/元组类型。

ts
type StringNumberBooleans = [string, number, ...boolean[]];
type StringBooleansNumber = [string, ...boolean[], number];
type BooleansStringNumber = [...boolean[], string, number];
Try
  • StringNumberBooleans 描述了一个元组,其前两个元素分别是 stringnumber,但后面可能还有任意数量的 boolean
  • StringBooleansNumber 描述了一个元组,其第一个元素是 string,然后是任意数量的 boolean,并以 number 结尾。
  • BooleansStringNumber 描述了一个元组,其开始元素是任意数量的 boolean,并以 stringnumber 结尾。

带有剩余元素的元组没有固定的“长度”——它只有一组在不同位置上众所周知的元素。

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

为什么可选元素和剩余元素有用?好吧,它允许 TypeScript 将元组与参数列表对应起来。元组类型可以用在剩余参数和实参中,因此以下内容

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

基本上等同于

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

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

readonly 元组类型

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

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

正如你所预料的那样,在 TypeScript 中,不允许向 readonly 元组的任何属性进行写入。

ts
function doSomething(pair: readonly [string, number]) {
pair[0] = "hello!";
Cannot assign to '0' because it is a read-only property.2540Cannot assign to '0' because it is a read-only property.
}
Try

大多数代码中的元组通常在创建后就不会被修改,因此尽可能将类型标注为 readonly 元组是一个很好的默认做法。考虑到带有 const 断言的数组字面量将被推断为 readonly 元组类型,这一点也很重要。

ts
let point = [3, 4] as const;
 
function distanceFromOrigin([x, y]: [number, number]) {
return Math.sqrt(x ** 2 + y ** 2);
}
 
distanceFromOrigin(point);
Argument of type 'readonly [3, 4]' is not assignable to parameter of type '[number, number]'. The type 'readonly [3, 4]' is 'readonly' and cannot be assigned to the mutable type '[number, number]'.2345Argument of type 'readonly [3, 4]' is not assignable to parameter of type '[number, number]'. The type 'readonly [3, 4]' is 'readonly' and cannot be assigned to the mutable type '[number, number]'.
Try

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

TypeScript 文档是一个开源项目。请通过发送 Pull Request 帮助我们改进这些页面 ❤

此页面的贡献者
DRDaniel Rosenwasser (52)
OTOrta Therox (16)
338elements  (2)
BRBruce Robertson (2)
ARAlan Rempel (2)
23+

最后更新:2026 年 3 月 27 日