对象类型

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

¥In JavaScript, the fundamental way that we group and pass around data is through objects. In TypeScript, we represent those through object types.

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

¥As we’ve seen, they can be anonymous:

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

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

¥or they can be named by using either an interface:

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

或类型别名:

¥or a type alias:

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

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

¥In all three examples above, we’ve written functions that take objects that contain the property name (which must be a string) and age (which must be a number).

快速参考

¥Quick Reference

如果你想快速浏览重要的日常语法,我们为 typeinterface 提供备忘单。

¥We have cheat-sheets available for both type and interface, if you want a quick look at the important every-day syntax at a glance.

属性修饰符

¥Property Modifiers

对象类型中的每个属性都可以指定一些内容:类型,属性是否可选,属性是否可以写入。

¥Each property in an object type can specify a couple of things: the type, whether the property is optional, and whether the property can be written to.

可选属性

¥Optional Properties

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

¥Much of the time, we’ll find ourselves dealing with objects that might have a property set. In those cases, we can mark those properties as optional by adding a question mark (?) to the end of their names.

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 的每个调用都是有效的。所有的可选性真正说明的是,如果设置了属性,它最好有一个特定的类型。

¥In this example, both xPos and yPos are considered optional. We can choose to provide either of them, so every call above to paintShape is valid. All optionality really says is that if the property is set, it better have a specific type.

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

¥We can also read from those properties - but when we do under strictNullChecks, TypeScript will tell us they’re potentially 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

¥In JavaScript, even if the property has never been set, we can still access it - it’s just going to give us the value undefined. We can just handle undefined specially by checking for it.

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 有语法来支持它。

¥Note that this pattern of setting defaults for unspecified values is so common that JavaScript has syntax to support it.

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 提供了 默认值。现在 xPosyPos 都肯定存在于 paintShape 的主体中,但对于 paintShape 的任何调用者都是可选的。

¥Here we used a destructuring pattern for paintShape’s parameter, and provided default values for xPos and yPos. Now xPos and yPos are both definitely present within the body of paintShape, but optional for any callers to paintShape.

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

¥Note that there is currently no way to place type annotations within destructuring patterns. This is because the following syntax already means something different in 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

¥In an object destructuring pattern, shape: Shape means “grab the property shape and redefine it locally as a variable named Shape. Likewise xPos: number creates a variable named number whose value is based on the parameter’s xPos.

readonly 属性

¥readonly Properties

对于 TypeScript,属性也可以标记为 readonly。虽然它不会在运行时改变任何行为,但在类型检查期间无法写入标记为 readonly 的属性。

¥Properties can also be marked as readonly for TypeScript. While it won’t change any behavior at runtime, a property marked as readonly can’t be written to during type-checking.

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 修饰符并不一定意味着值是完全不可变的 - 或者换句话说,其内部内容无法更改。这只是意味着属性本身不能被重写。

¥Using the readonly modifier doesn’t necessarily imply that a value is totally immutable - or in other words, that its internal contents can’t be changed. It just means the property itself can’t be re-written to.

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 属性也可以通过别名来更改。

¥It’s important to manage expectations of what readonly implies. It’s useful to signal intent during development time for TypeScript on how an object should be used. TypeScript doesn’t factor in whether properties on two types are readonly when checking whether those types are compatible, so readonly properties can also change via aliasing.

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

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

¥Using mapping modifiers, you can remove readonly attributes.

索引签名

¥Index Signatures

有时你并不提前知道类型属性的所有名称,但你确实知道值的形状。

¥Sometimes you don’t know all the names of a type’s properties ahead of time, but you do know the shape of the values.

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

¥In those cases you can use an index signature to describe the types of possible values, for example:

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

上面,我们有一个 StringArray 接口,它有一个索引签名。这个索引签名表明当一个 StringArray 被一个 number 索引时,它将返回一个 string

¥Above, we have a StringArray interface which has an index signature. This index signature states that when a StringArray is indexed with a number, it will return a string.

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

¥Only some types are allowed for index signature properties: string, number, symbol, template string patterns, and union types consisting only of these.

可以支持两种类型的索引器......

可以支持两种类型的索引器,但从数字索引器返回的类型必须是从字符串索引器返回的类型的子类型。这是因为当使用 number 进行索引时,JavaScript 在索引到对象之前实际上会将其转换为 string。这意味着使用 XSPACE100number)建立索引与使用 "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

虽然字符串索引签名是描述 “dictionary” 模式的强大方式,但它们还强制所有属性与其返回类型匹配。这是因为字符串索引声明 obj.property 也可用作 obj["property"]。在下面的例子中,name 的类型与字符串索引的类型不匹配,类型检查器给出错误:

¥While string index signatures are a powerful way to describe the “dictionary” pattern, they also enforce that all properties match their return type. This is because a string index declares that obj.property is also available as obj["property"]. In the following example, name’s type does not match the string index’s type, and the type checker gives an error:

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

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

¥However, properties of different types are acceptable if the index signature is a union of the property types:

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

最后,你可以使索引签名 readonly 以防止分配给它们的索引:

¥Finally, you can make index signatures readonly in order to prevent assignment to their indices:

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

¥You can’t set myArray[2] because the index signature is readonly.

溢出属性检查

¥Excess Property Checks

在何处以及如何为对象分配类型可以在类型系统中产生差异。这方面的一个关键示例是溢出属性检查,它会在创建对象时更彻底地验证对象并在创建期间将其分配给对象类型。

¥Where and how an object is assigned a type can make a difference in the type system. One of the key examples of this is in excess property checking, which validates the object more thoroughly when it is created and assigned to an object type during creation.

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 });
Argument of type '{ colour: string; width: number; }' is not assignable to parameter of type 'SquareConfig'. Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?2345Argument of type '{ colour: string; width: number; }' is not assignable to parameter of type 'SquareConfig'. Object 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 中,这种事情会悄无声息地失败。

¥Notice the given argument to createSquare is spelled colour instead of color. In plain JavaScript, this sort of thing fails silently.

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

¥You could argue that this program is correctly typed, since the width properties are compatible, there’s no color property present, and the extra colour property is insignificant.

但是,TypeScript 认为这段代码中可能存在错误。对象字面在将它们分配给其他变量或将它们作为参数传递时会得到特殊处理并进行额外的属性检查。如果一个对象字面量有任何 “目标类型” 没有的属性,你会得到一个错误:

¥However, TypeScript takes the stance that there’s probably a bug in this code. Object literals get special treatment and undergo excess property checking when assigning them to other variables, or passing them as arguments. If an object literal has any properties that the “target type” doesn’t have, you’ll get an error:

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

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

¥Getting around these checks is actually really simple. The easiest method is to just use a type assertion:

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

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

¥However, a better approach might be to add a string index signature if you’re sure that the object can have some extra properties that are used in some special way. If SquareConfig can have color and width properties with the above types, but could also have any number of other properties, then we could define it like so:

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

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

¥Here we’re saying that SquareConfig can have any number of properties, and as long as they aren’t color or width, their types don’t matter.

绕过这些检查的最后一种方法(可能有点令人惊讶)是将对象分配给另一个变量:由于分配 squareOptions 不会进行溢出属性检查,因此编译器不会给你错误:

¥One final way to get around these checks, which might be a bit surprising, is to assign the object to another variable: Since assigning squareOptions won’t undergo excess property checks, the compiler won’t give you an error:

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

只要你在 squareOptionsSquareConfig 之间具有共同属性,上述变通方法就会起作用。在此示例中,它是属性 width。但是,如果变量没有任何公共对象属性,它将失败。例如:

¥The above workaround will work as long as you have a common property between squareOptions and SquareConfig. In this example, it was the property width. It will however, fail if the variable does not have any common object property. For example:

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

请记住,对于像上面这样的简单代码,你可能不应该尝试对这些检查进行 “到处走走”。对于具有方法和保持状态的更复杂的对象字面量,你可能需要牢记这些技术,但大多数溢出属性错误实际上是错误。

¥Keep in mind that for simple code like above, you probably shouldn’t be trying to “get around” these checks. For more complex object literals that have methods and hold state, you might need to keep these techniques in mind, but a majority of excess property errors are actually bugs.

这意味着如果你遇到诸如选项包之类的溢出属性检查问题,你可能需要修改一些类型声明。在这种情况下,如果可以将具有 colorcolour 属性的对象传递给 createSquare,则应该修改 SquareConfig 的定义以反映这一点。

¥That means if you’re running into excess property checking problems for something like option bags, you might need to revise some of your type declarations. In this instance, if it’s okay to pass an object with both a color or colour property to createSquare, you should fix up the definition of SquareConfig to reflect that.

扩展类型

¥Extending Types

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

¥It’s pretty common to have types that might be more specific versions of other types. For example, we might have a BasicAddress type that describes the fields necessary for sending letters and packages in the U.S.

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

在某些情况下这就足够了,但如果某个地址的楼房有多个单元,则地址通常有一个与之关联的单元号。然后我们可以描述一个 AddressWithUnit

¥In some situations that’s enough, but addresses often have a unit number associated with them if the building at an address has multiple units. We can then describe an AddressWithUnit.

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

这可以完成工作,但这里的缺点是当我们的更改纯粹是添加时,我们必须重复 BasicAddress 中的所有其他字段。相反,我们可以扩展原来的 BasicAddress 类型,只添加 AddressWithUnit 独有的新字段。

¥This does the job, but the downside here is that we had to repeat all the other fields from BasicAddress when our changes were purely additive. Instead, we can extend the original BasicAddress type and just add the new fields that are unique to 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,所以读者会知道这两种类型在某种程度上是相关的。

¥The extends keyword on an interface allows us to effectively copy members from other named types, and add whatever new members we want. This can be useful for cutting down the amount of type declaration boilerplate we have to write, and for signaling intent that several different declarations of the same property might be related. For example, AddressWithUnit didn’t need to repeat the street property, and because street originates from BasicAddress, a reader will know that those two types are related in some way.

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

¥interfaces can also extend from multiple types.

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 提供了另一种称为交叉类型的构造,主要用于组合现有的对象类型。

¥interfaces allowed us to build up new types from other types by extending them. TypeScript provides another construct called intersection types that is mainly used to combine existing object types.

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

¥An intersection type is defined using the & operator.

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

在这里,我们将 ColorfulCircle 相交以生成一个包含 ColorfulCircle 的所有成员的新类型。

¥Here, we’ve intersected Colorful and Circle to produce a new type that has all the members of Colorful and 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 });
Argument of type '{ color: string; raidus: number; }' is not assignable to parameter of type 'Colorful & Circle'. Object literal may only specify known properties, but 'raidus' does not exist in type 'Colorful & Circle'. Did you mean to write 'radius'?2345Argument of type '{ color: string; raidus: number; }' is not assignable to parameter of type 'Colorful & Circle'. Object literal may only specify known properties, but 'raidus' does not exist in type 'Colorful & Circle'. Did you mean to write 'radius'?
Try

接口与交叉

¥Interfaces vs. Intersections

我们只是研究了两种组合相似但实际上略有不同的类型的方法。使用接口,我们可以使用 extends 子句从其他类型扩展,我们可以对交叉做类似的事情,并用类型别名命名结果。两者之间的主要区别在于冲突的处理方式,这种区别通常是你在接口和交叉类型的类型别名之间选择一个而不是另一个的主要原因之一。

¥We just looked at two ways to combine types which are similar, but are actually subtly different. With interfaces, we could use an extends clause to extend from other types, and we were able to do something similar with intersections and name the result with a type alias. The principal difference between the two is how conflicts are handled, and that difference is typically one of the main reasons why you’d pick one over the other between an interface and a type alias of an intersection type.

泛型对象类型

¥Generic Object Types

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

¥Let’s imagine a Box type that can contain any value - strings, numbers, Giraffes, whatever.

ts
interface Box {
contents: any;
}
Try

现在,contents 属性的类型为 any,虽然有效,但可能会导致事故发生。

¥Right now, the contents property is typed as any, which works, but can lead to accidents down the line.

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

¥We could instead use unknown, but that would mean that in cases where we already know the type of contents, we’d need to do precautionary checks, or use error-prone type assertions.

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 类型。

¥One type safe approach would be to instead scaffold out different Box types for every type of contents.

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

但这意味着我们必须创建不同的函数或函数重载,才能对这些类型进行操作。

¥But that means we’ll have to create different functions, or overloads of functions, to operate on these types.

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

这是很多样板。此外,我们稍后可能需要引入新的类型和重载。这令人沮丧,因为我们的盒子类型和重载实际上都是相同的。

¥That’s a lot of boilerplate. Moreover, we might later need to introduce new types and overloads. This is frustrating, since our box types and overloads are all effectively the same.

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

¥Instead, we can make a generic Box type which declares a type parameter.

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

你可能会将其理解为“TypeBox 是其 contents 具有类型 Type 的东西”。稍后,当我们引用 Box 时,我们必须给出一个类型参数来代替 Type

¥You might read this as “A Box of Type is something whose contents have type Type”. Later on, when we refer to Box, we have to give a type argument in place of Type.

ts
let box: Box<string>;
Try

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

¥Think of Box as a template for a real type, where Type is a placeholder that will get replaced with some other type. When TypeScript sees Box<string>, it will replace every instance of Type in Box<Type> with string, and end up working with something like { contents: string }. In other words, Box<string> and our earlier StringBox work identically.

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 类型(尽管如果我们愿意,我们当然可以)。

¥Box is reusable in that Type can be substituted with anything. That means that when we need a box for a new type, we don’t need to declare a new Box type at all (though we certainly could if we wanted to).

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

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

¥This also means that we can avoid overloads entirely by instead using generic functions.

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

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

¥It is worth noting that type aliases can also be generic. We could have defined our new Box<Type> interface, which was:

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

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

¥by using a type alias instead:

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

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

¥Since type aliases, unlike interfaces, can describe more than just object types, we can also use them to write other kinds of generic helper types.

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

稍后我们将回过头来输入别名。

¥We’ll circle back to type aliases in just a little bit.

Array 类型

¥The Array Type

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

¥Generic object types are often some sort of container type that work independently of the type of elements they contain. It’s ideal for data structures to work this way so that they’re re-usable across different data types.

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

¥It turns out we’ve been working with a type just like that throughout this handbook: the Array type. Whenever we write out types like number[] or string[], that’s really just a shorthand for Array<number> and 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 本身是一个泛型类型。

¥Much like the Box type above, Array itself is a generic type.

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 的行为方式,它们可以与任何类型的集合一起使用。

¥Modern JavaScript also provides other data structures which are generic, like Map<K, V>, Set<T>, and Promise<T>. All this really means is that because of how Map, Set, and Promise behave, they can work with any sets of types.

ReadonlyArray 类型

¥The ReadonlyArray Type

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

¥The ReadonlyArray is a special type that describes arrays that shouldn’t be changed.

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 的函数时,它告诉我们可以将任何数组传递到该函数中,而不必担心它会更改其内容。

¥Much like the readonly modifier for properties, it’s mainly a tool we can use for intent. When we see a function that returns ReadonlyArrays, it tells us we’re not meant to change the contents at all, and when we see a function that consumes ReadonlyArrays, it tells us that we can pass any array into that function without worrying that it will change its contents.

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

¥Unlike Array, there isn’t a ReadonlyArray constructor that we can use.

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

¥Instead, we can assign regular Arrays to ReadonlyArrays.

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

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

¥Just as TypeScript provides a shorthand syntax for Array<Type> with Type[], it also provides a shorthand syntax for ReadonlyArray<Type> with 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 之间不是双向的。

¥One last thing to note is that unlike the readonly property modifier, assignability isn’t bidirectional between regular Arrays and ReadonlyArrays.

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 类型,它确切地知道它包含多少个元素,以及它在特定位置包含哪些类型。

¥A tuple type is another sort of Array type that knows exactly how many elements it contains, and exactly which types it contains at specific positions.

ts
type StringNumberPair = [string, number];
Try

这里,StringNumberPairstringnumber 的元组类型。与 ReadonlyArray 一样,它在运行时没有表示,但对 TypeScript 很重要。对于类型系统,StringNumberPair 描述了 0 索引包含 string1 索引包含 number 的数组。

¥Here, StringNumberPair is a tuple type of string and number. Like ReadonlyArray, it has no representation at runtime, but is significant to TypeScript. To the type system, StringNumberPair describes arrays whose 0 index contains a string and whose 1 index contains a 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

如果我们试图索引超过元素的数量,我们会得到一个错误。

¥If we try to index past the number of elements, we’ll get an error.

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 的数组解构来 解构元组

¥We can also destructure tuples using JavaScript’s array destructuring.

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 命名为我们想要的任何名称。

¥Tuple types are useful in heavily convention-based APIs, where each element’s meaning is “obvious”. This gives us flexibility in whatever we want to name our variables when we destructure them. In the above example, we were able to name elements 0 and 1 to whatever we wanted.

但是,由于并非每个用户都对显而易见的事物持有相同的看法,因此可能值得重新考虑使用具有描述性属性名称的对象是否更适合你的 API。

¥However, since not every user holds the same view of what’s obvious, it may be worth reconsidering whether using objects with descriptive property names may be better for your API.

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

¥Other than those length checks, simple tuple types like these are equivalent to types which are versions of Arrays that declare properties for specific indexes, and that declare length with a numeric literal type.

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 的类型。

¥Another thing you may be interested in is that tuples can have optional properties by writing out a question mark (? after an element’s type). Optional tuple elements can only come at the end, and also affect the type of 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

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

¥Tuples can also have rest elements, which have to be an array/tuple type.

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

    ¥StringNumberBooleans describes a tuple whose first two elements are string and number respectively, but which may have any number of booleans following.

  • StringBooleansNumber 描述一个元组,其第一个元素是 string,然后是任意数量的 boolean,最后以 number 结尾。

    ¥StringBooleansNumber describes a tuple whose first element is string and then any number of booleans and ending with a number.

  • BooleansStringNumber 描述了一个元组,其起始元素是任意数量的 boolean,并以 stringnumber 结尾。

    ¥BooleansStringNumber describes a tuple whose starting elements are any number of booleans and ending with a string then a number.

具有剩余元素的元组没有设置 “length” - 它只有一组不同位置的众所周知的元素。

¥A tuple with a rest element has no set “length” - it only has a set of well-known elements in different positions.

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

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

¥Why might optional and rest elements be useful? Well, it allows TypeScript to correspond tuples with parameter lists. Tuples types can be used in rest parameters and arguments, so that the following:

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

基本上相当于:

¥is basically equivalent to:

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

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

¥This is handy when you want to take a variable number of arguments with a rest parameter, and you need a minimum number of elements, but you don’t want to introduce intermediate variables.

readonly 元组类型

¥readonly Tuple Types

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

¥One final note about tuple types - tuple types have readonly variants, and can be specified by sticking a readonly modifier in front of them - just like with array shorthand syntax.

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

正如你所料,TypeScript 中不允许写入 readonly 元组的任何属性。

¥As you might expect, writing to any property of a readonly tuple isn’t allowed in TypeScript.

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 元组类型来推断。

¥Tuples tend to be created and left un-modified in most code, so annotating types as readonly tuples when possible is a good default. This is also important given that array literals with const assertions will be inferred with readonly tuple types.

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 的元素不会发生修改。

¥Here, distanceFromOrigin never modifies its elements, but expects a mutable tuple. Since point’s type was inferred as readonly [3, 4], it won’t be compatible with [number, number] since that type can’t guarantee point’s elements won’t be mutated.