在 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:
tsTryfunctiongreet (person : {name : string;age : number }) {return "Hello " +person .name ;}
或者它们可以通过使用接口来命名:
🌐 or they can be named by using either an interface:
tsTryinterfacePerson {name : string;age : number;}functiongreet (person :Person ) {return "Hello " +person .name ;}
或类型别名:
🌐 or a type alias:
tsTrytypePerson = {name : string;age : number;};functiongreet (person :Person ) {return "Hello " +person .name ;}
在上面所有三个例子中,我们都编写了函数,这些函数接受包含属性 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
我们为 type 和 interface 提供了备忘单,如果你想快速浏览日常重要语法的话,这将非常方便。
🌐 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.
tsTryinterfacePaintOptions {shape :Shape ;xPos ?: number;yPos ?: number;}functionpaintShape (opts :PaintOptions ) {// ...}constshape =getShape ();paintShape ({shape });paintShape ({shape ,xPos : 100 });paintShape ({shape ,yPos : 100 });paintShape ({shape ,xPos : 100,yPos : 100 });
在这个例子中,xPos 和 yPos 都被视为可选项。我们可以选择提供其中的任意一个,因此上面对 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.
tsTryfunctionpaintShape (opts :PaintOptions ) {letxPos =opts .xPos ;letyPos =opts .yPos ;// ...}
在 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.
tsTryfunctionpaintShape (opts :PaintOptions ) {letxPos =opts .xPos ===undefined ? 0 :opts .xPos ;letyPos =opts .yPos ===undefined ? 0 :opts .yPos ;// ...}
请注意,这种为未指定值设置默认值的模式非常普遍,以至于 JavaScript 有语法来支持它。
🌐 Note that this pattern of setting defaults for unspecified values is so common that JavaScript has syntax to support it.
tsTryfunctionpaintShape ({shape ,xPos = 0,yPos = 0 }:PaintOptions ) {console .log ("x coordinate at",xPos );console .log ("y coordinate at",yPos );// ...}
在这里,我们为 paintShape 的参数使用了 解构模式,并为 xPos 和 yPos 提供了 默认值。现在 xPos 和 yPos 在 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 中已经表示不同的含义。
tsTryfunctiondraw ({shape :Shape ,xPos :number = 100 /*...*/ }) {Cannot find name 'shape'. Did you mean 'Shape'?2552Cannot find name 'shape'. Did you mean 'Shape'?render (); shape Cannot find name 'xPos'.2304Cannot find name 'xPos'.render (); xPos }In an object destructuring pattern,
shape: Shapemeans “grab the propertyshapeand redefine it locally as a variable namedShape.” LikewisexPos: numbercreates a variable namednumberwhose value is based on the parameter’sxPos。
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.
tsTryinterfaceSomeType {readonlyprop : string;}functiondoSomething (obj :SomeType ) {// We can read from 'obj.prop'.console .log (`prop has the value '${obj .prop }'.`);// But we can't re-assign it.Cannot assign to 'prop' because it is a read-only property.2540Cannot assign to 'prop' because it is a read-only property.obj .= "hello"; prop }
使用 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.
tsTryinterfaceHome {readonlyresident : {name : string;age : number };}functionvisitForBirthday (home :Home ) {// We can read and update properties from 'home.resident'.console .log (`Happy birthday ${home .resident .name }!`);home .resident .age ++;}functionevict (home :Home ) {// But we can't write to the 'resident' property itself on a 'Home'.Cannot assign to 'resident' because it is a read-only property.2540Cannot assign to 'resident' because it is a read-only property.home .= { resident name : "Victor the Evictor",age : 42,};}
管理对 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.
tsTryinterfacePerson {name : string;age : number;}interfaceReadonlyPerson {readonlyname : string;readonlyage : number;}letwritablePerson :Person = {name : "Person McPersonface",age : 42,};// worksletreadonlyPerson :ReadonlyPerson =writablePerson ;console .log (readonlyPerson .age ); // prints '42'writablePerson .age ++;console .log (readonlyPerson .age ); // prints '43'
使用 映射修饰符,你可以移除 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:
tsTryinterfaceStringArray {[index : number]: string;}constmyArray :StringArray =getStringArray ();constsecondItem =myArray [1];
上面,我们有一个带有索引签名的 StringArray 接口。这个索引签名表明,当使用 number 索引 StringArray 时,它将返回一个 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.
索引签名属性只允许某些类型:string、number、symbol、模板字符串模式,以及仅由这些类型组成的联合类型。
🌐 Only some types are allowed for index signature properties: string, number, symbol, template string patterns, and union types consisting only of these.
可以支持多种类型的索引器...
可以支持多种类型的索引器。请注意,当同时使用 `number` 和 `string` 索引器时,从数字索引器返回的类型必须是从字符串索引器返回的类型的子类型。这是因为在使用 number 进行索引时,JavaScript 实际上会在对对象进行索引之前将其转换为 string。这意味着用 100(一个 number)索引,与用 "100"(一个 string)索引是相同的,因此两者需要保持一致。
tsTryinterfaceAnimal {name : string;}interfaceDog extendsAnimal {breed : string;}// Error: indexing with a numeric string might get you a completely separate type of Animal!interfaceNotOkay {['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 : number]:Animal ;[x : string]:Dog ;}
虽然字符串索引签名是描述“字典”模式的强大方式,但它们也要求所有属性都必须与返回类型匹配。这是因为字符串索引声明了 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:
tsTryinterfaceNumberDictionary {[index : string]: number;length : number; // okProperty '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'.: string; name }
但是,如果索引签名是属性类型的联合,则可以接受不同类型的属性:
🌐 However, properties of different types are acceptable if the index signature is a union of the property types:
tsTryinterfaceNumberOrStringDictionary {[index : string]: number | string;length : number; // ok, length is a numbername : string; // ok, name is a string}
最后,你可以创建索引签名 readonly 来防止对其索引进行赋值:
🌐 Finally, you can make index signatures readonly in order to prevent assignment to their indices:
tsTryinterfaceReadonlyStringArray {readonly [index : number]: string;}letmyArray :ReadonlyStringArray =getReadOnlyStringArray ();Index signature in type 'ReadonlyStringArray' only permits reading.2542Index signature in type 'ReadonlyStringArray' only permits reading.myArray [2] = "Mallory";
你无法设置 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.
tsTryinterfaceSquareConfig {color ?: string;width ?: number;}functioncreateSquare (config :SquareConfig ): {color : string;area : number } {return {color :config .color || "red",area :config .width ?config .width *config .width : 20,};}letObject 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'?mySquare =createSquare ({: "red", colour width : 100 });
注意传递给 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:
tsTryletObject 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'?mySquare =createSquare ({: "red", colour width : 100 });
绕过这些检查其实非常简单。最简单的方法就是使用类型断言:
🌐 Getting around these checks is actually really simple. The easiest method is to just use a type assertion:
tsTryletmySquare =createSquare ({width : 100,opacity : 0.5 } asSquareConfig );
然而,更好的方法可能是添加一个字符串索引签名,如果你确定该对象可以有一些以特殊方式使用的额外属性的话。
如果 SquareConfig 可以有 color 和 width 属性,其类型如上所述,但也可能有任意数量的其他属性,那么我们可以这样定义它:
🌐 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:
tsTryinterfaceSquareConfig {color ?: string;width ?: number;[propName : string]: unknown;}
这里我们说的是 SquareConfig 可以有任意数量的属性,只要它们不是 color 或 width,它们的类型无关紧要。
🌐 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:
tsTryletsquareOptions = {colour : "red",width : 100 };letmySquare =createSquare (squareOptions );
只要 squareOptions 和 SquareConfig 之间有一个共同的属性,上述方法就可以工作。在这个例子中,它是属性 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:
tsTryletsquareOptions = {colour : "red" };letType '{ colour: string; }' has no properties in common with type 'SquareConfig'.2559Type '{ colour: string; }' has no properties in common with type 'SquareConfig'.mySquare =createSquare (); squareOptions
请记住,对于像上面这样的简单代码,你可能不应该尝试“绕过”这些检查。对于具有方法并保存状态的更复杂的对象字面量,你可能需要记住这些技巧,但大多数多余属性错误实际上是程序错误。
🌐 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.
这意味着,如果你在处理类似选项包之类的情况时遇到过多属性检查的问题,你可能需要修改某些类型声明。在这种情况下,如果允许将同时具有 color 或 colour 属性的对象传递给 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.
tsTryinterfaceBasicAddress {name ?: string;street : string;city : string;country : string;postalCode : string;}
在某些情况下,这已经足够了,但如果一个地址的架构有多个单元,地址通常会有单元号。我们可以用 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.
tsTryinterfaceAddressWithUnit {name ?: string;unit : string;street : string;city : string;country : string;postalCode : string;}
这可以完成任务,但缺点是当我们的更改只是附加时,我们必须重复 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.
tsTryinterfaceBasicAddress {name ?: string;street : string;city : string;country : string;postalCode : string;}interfaceAddressWithUnit extendsBasicAddress {unit : string;}
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 也可以从多种类型扩展而来。
tsTryinterfaceColorful {color : string;}interfaceCircle {radius : number;}interfaceColorfulCircle extendsColorful ,Circle {}constcc :ColorfulCircle = {color : "red",radius : 42,};
交叉类型
🌐 Intersection Types
interface 允许我们通过扩展其他类型来构建新类型。 TypeScript 提供了另一种称为 交叉类型 的构造,主要用于组合现有的对象类型。
交叉类型是使用 & 运算符定义的。
🌐 An intersection type is defined using the & operator.
tsTryinterfaceColorful {color : string;}interfaceCircle {radius : number;}typeColorfulCircle =Colorful &Circle ;
在这里,我们交叉了 Colorful 和 Circle 来生成一个新类型,该类型包含 Colorful 和 Circle 的所有成员。
🌐 Here, we’ve intersected Colorful and Circle to produce a new type that has all the members of Colorful and Circle.
tsTryfunctiondraw (circle :Colorful &Circle ) {console .log (`Color was ${circle .color }`);console .log (`Radius was ${circle .radius }`);}// okaydraw ({color : "blue",radius : 42 });// oopsObject 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'?draw ({color : "red",: 42 }); raidus
接口扩展与交叉
🌐 Interface Extension vs. Intersection
我们刚刚看了两种组合类型的方法,这两种方法看起来相似,但实际上有细微的差别。使用接口时,我们可以使用 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.
如果使用相同的名称定义接口,TypeScript 会尝试合并它们,如果属性是兼容的。如果属性不兼容(即它们具有相同的属性名称但类型不同),TypeScript 将会报错。
🌐 If interfaces are defined with the same name, TypeScript will attempt to merge them if the properties are compatible. If the properties are not compatible (i.e., they have the same property name but different types), TypeScript will raise an error.
在交叉类型的情况下,不同类型的属性会自动合并。当该类型在后续使用时,TypeScript 会期望该属性同时满足两种类型,这可能会产生意想不到的结果。
🌐 In the case of intersection types, properties with different types will be merged automatically. When the type is used later, TypeScript will expect the property to satisfy both types simultaneously, which may produce unexpected results.
例如,以下代码将抛出错误,因为属性不兼容:
🌐 For example, the following code will throw an error because the properties are incompatible:
tsinterface Person {name: string;}interface Person {name: number;}
相比之下,以下代码可以编译,但结果是 never 类型:
🌐 In contrast, the following code will compile, but it results in a never type:
tsTryinterfacePerson1 {name : string;}interfacePerson2 {name : number;}typeStaff =Person1 &Person2 declare conststaffer :Staff ;staffer .name ;
In this case, Staff would require the name property to be both a string and a number, which results in property being of type never.
泛型对象类型
🌐 Generic Object Types
让我们想象一种 Box 类型,它可以包含任何值——string、number、Giraffe,或者其他任何东西。
🌐 Let’s imagine a Box type that can contain any value - strings, numbers, Giraffes, whatever.
tsTryinterfaceBox {contents : any;}
目前,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.
tsTryinterfaceBox {contents : unknown;}letx :Box = {contents : "hello world",};// we could check 'x.contents'if (typeofx .contents === "string") {console .log (x .contents .toLowerCase ());}// or we could use a type assertionconsole .log ((x .contents as string).toLowerCase ());
一种类型安全的方法是为每种类型的 contents 搭建不同的 Box 类型。
🌐 One type safe approach would be to instead scaffold out different Box types for every type of contents.
tsTryinterfaceNumberBox {contents : number;}interfaceStringBox {contents : string;}interfaceBooleanBox {contents : boolean;}
但这意味着我们必须创建不同的函数或函数重载,才能对这些类型进行操作。
🌐 But that means we’ll have to create different functions, or overloads of functions, to operate on these types.
tsTryfunctionsetContents (box :StringBox ,newContents : string): void;functionsetContents (box :NumberBox ,newContents : number): void;functionsetContents (box :BooleanBox ,newContents : boolean): void;functionsetContents (box : {contents : any },newContents : any) {box .contents =newContents ;}
那是大量的样板代码。此外,我们之后可能需要引入新的类型和重载。这令人沮丧,因为我们的盒子类型和重载实际上都是相同的。
🌐 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.
tsTryinterfaceBox <Type > {contents :Type ;}
你可以将其读作“Type 的 Box 是其 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.
tsTryletbox :Box <string>;
把 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.
tsTryinterfaceBox <Type > {contents :Type ;}interfaceStringBox {contents : string;}letboxA :Box <string> = {contents : "hello" };boxA .contents ;letboxB :StringBox = {contents : "world" };boxB .contents ;
Box 是可重复使用的,因为 Type 可以替换成任何东西。这意味着当我们需要为新类型创建一个盒子时,我们根本不需要声明一个新的 Box 类型(当然,如果我们愿意,也完全可以这么做)。
tsTryinterfaceBox <Type > {contents :Type ;}interfaceApple {// ....}// Same as '{ contents: Apple }'.typeAppleBox =Box <Apple >;
这也意味着,我们可以完全避免使用重载,而改用泛型函数。
🌐 This also means that we can avoid overloads entirely by instead using generic functions.
tsTryfunctionsetContents <Type >(box :Box <Type >,newContents :Type ) {box .contents =newContents ;}
值得注意的是,类型别名也可以是泛型的。我们本可以定义我们新的 Box<Type> 接口,如下所示:
🌐 It is worth noting that type aliases can also be generic. We could have defined our new Box<Type> interface, which was:
tsTryinterfaceBox <Type > {contents :Type ;}
通过使用类型别名来代替:
🌐 by using a type alias instead:
tsTrytypeBox <Type > = {contents :Type ;};
由于类型别名与接口不同,它可以描述的不仅仅是对象类型,我们也可以使用它们来编写其他类型的泛型辅助程序类型。
🌐 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.
tsTrytypeOrNull <Type > =Type | null;typeOneOrMany <Type > =Type |Type [];typeOneOrManyOrNull <Type > =OrNull <OneOrMany <Type >>;typeOneOrManyOrNullStrings =OneOrManyOrNull <string>;
稍后我们将回过头来输入别名。
🌐 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>.
tsTryfunctiondoSomething (value :Array <string>) {// ...}letmyArray : string[] = ["hello", "world"];// either of these work!doSomething (myArray );doSomething (newArray ("hello", "world"));
与上面的 Box 类型类似,Array 本身也是一个泛型类型。
🌐 Much like the Box type above, Array itself is a generic type.
tsTryinterfaceArray <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;// ...}
现代 JavaScript 还提供了其他通用的数据结构,如 Map<K, V>、Set<T> 和 Promise<T>。这实际上意味着,由于 Map、Set 和 Promise 的行为方式,它们可以与任何类型集合一起使用。
🌐 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.
tsTryfunctiondoStuff (values :ReadonlyArray <string>) {// We can read from 'values'...constcopy =values .slice ();console .log (`The first value is ${values [0]}`);// ...but we can't mutate 'values'.Property 'push' does not exist on type 'readonly string[]'.2339Property 'push' does not exist on type 'readonly string[]'.values .("hello!"); push }
就像属性的 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.
tsTrynew'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.("red", "green", "blue"); ReadonlyArray
相反,我们可以将常规的 Array 分配给 ReadonlyArray。
🌐 Instead, we can assign regular Arrays to ReadonlyArrays.
tsTryconstroArray :ReadonlyArray <string> = ["red", "green", "blue"];
正如 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[].
tsTryfunctiondoStuff (values : readonly string[]) {// We can read from 'values'...constcopy =values .slice ();console .log (`The first value is ${values [0]}`);// ...but we can't mutate 'values'.Property 'push' does not exist on type 'readonly string[]'.2339Property 'push' does not exist on type 'readonly string[]'.values .("hello!"); push }
最后需要注意的是,与 readonly 属性修饰符不同,常规 Array 和 ReadonlyArray 之间的可赋值性不是双向的。
🌐 One last thing to note is that unlike the readonly property modifier, assignability isn’t bidirectional between regular Arrays and ReadonlyArrays.
tsTryletx : readonly string[] = [];lety : string[] = [];x =y ;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[]'.= y x ;
元组类型
🌐 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.
tsTrytypeStringNumberPair = [string, number];
这里,StringNumberPair 是一个由 string 和 number 组成的元组类型。像 ReadonlyArray 一样,它在运行时没有表示,但对 TypeScript 很重要。对于类型系统来说,StringNumberPair 描述了数组,其中 0 索引包含一个 string,而 1 索引包含一个 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.
tsTryfunctiondoSomething (pair : [string, number]) {consta =pair [0];constb =pair [1];// ...}doSomething (["hello", 42]);
如果我们试图索引超过元素的数量,我们会得到一个错误。
🌐 If we try to index past the number of elements, we’ll get an error.
tsTryfunctiondoSomething (pair : [string, number]) {// ...constTuple 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'.c =pair [2 ];}
我们也可以使用 JavaScript 的数组解构来解构元组。
🌐 We can also destructure tuples using JavaScript’s array destructuring.
tsTryfunctiondoSomething (stringHash : [string, number]) {const [inputString ,hash ] =stringHash ;console .log (inputString );console .log (hash );}
元组类型在高度依赖约定的 API 中非常有用,在这种情况下,每个元素的含义都是“显而易见的”。 这让我们在解构变量时可以灵活地命名它们。 在上面的例子中,我们可以将元素命名为
0和1,随意命名。然而,由于并不是每个用户对“显而易见”的理解都相同,所以值得重新考虑使用具有描述性属性名称的对象是否对你的 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.
tsTryinterfaceStringNumberPair {// specialized propertieslength : 2;0: string;1: number;// Other 'Array<string | number>' members...slice (start ?: number,end ?: number):Array <string | number>;}
你可能感兴趣的另一件事是,元组可以通过在元素类型后面写问号(?)来拥有可选属性。可选的元组元素只能出现在末尾,同时也会影响 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.
tsTrytypeEither2dOr3d = [number, number, number?];functionsetCoordinate (coord :Either2dOr3d ) {const [x ,y ,z ] =coord ;console .log (`Provided coordinates had ${coord .length } dimensions`);}
元组也可以有剩余元素,它们必须是数组/元组类型。
🌐 Tuples can also have rest elements, which have to be an array/tuple type.
tsTrytypeStringNumberBooleans = [string, number, ...boolean[]];typeStringBooleansNumber = [string, ...boolean[], number];typeBooleansStringNumber = [...boolean[], string, number];
StringNumberBooleans描述了一个元组,其前两个元素分别是string和number,但之后可以有任意数量的boolean。StringBooleansNumber描述了一个元组,其第一个元素是string,然后是任意数量的boolean,最后以number结尾。BooleansStringNumber描述了一个元组,其开头元素可以有任意数量的boolean,最后依次是string和number。
带有剩余元素的元组没有固定的“长度”——它只有在不同位置上的一组已知元素。
🌐 A tuple with a rest element has no set “length” - it only has a set of well-known elements in different positions.
tsTryconsta :StringNumberBooleans = ["hello", 1];constb :StringNumberBooleans = ["beautiful", 2, true];constc :StringNumberBooleans = ["world", 3, true, false, true, false, true];
可选元素和剩余元素可能有什么用处? 嗯,它允许 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:
tsTryfunctionreadButtonInput (...args : [string, number, ...boolean[]]) {const [name ,version , ...input ] =args ;// ...}
基本上相当于:
🌐 is basically equivalent to:
tsTryfunctionreadButtonInput (name : string,version : number, ...input : boolean[]) {// ...}
当你想用一个剩余参数获取可变数量的参数时,这很方便,并且你需要最少数量的元素,但你不想引入中间变量。
🌐 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.
tsTryfunctiondoSomething (pair : readonly [string, number]) {// ...}
正如你可能预料的那样,在 TypeScript 中不允许写入 readonly 元组的任何属性。
🌐 As you might expect, writing to any property of a readonly tuple isn’t allowed in TypeScript.
tsTryfunctiondoSomething (pair : readonly [string, number]) {Cannot assign to '0' because it is a read-only property.2540Cannot assign to '0' because it is a read-only property.pair [0 ] = "hello!";}
在大多数代码中,元组倾向于被创建后不被修改,因此在可能的情况下将类型标注为 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.
tsTryletpoint = [3, 4] asconst ;functiondistanceFromOrigin ([x ,y ]: [number, number]) {returnMath .sqrt (x ** 2 +y ** 2);}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]'.distanceFromOrigin (); point
在这里,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.