TypeScript 的核心原则之一是类型检查侧重于值的形状。这有时被称为“鸭子类型”或“结构子类型”。在 TypeScript 中,接口充当命名这些类型的角色,是定义代码内部以及与项目外部代码的契约的强大方式。
我们的第一个接口
了解接口工作原理最简单的方法是从一个简单的例子开始。
tsTry
functionprintLabel (labeledObj : {label : string }) {console .log (labeledObj .label );}letmyObj = {size : 10,label : "Size 10 Object" };printLabel (myObj );
类型检查器检查对 printLabel
的调用。printLabel
函数有一个参数,要求传入的对象必须有一个名为 label
的属性,类型为 string
。请注意,我们的对象实际上比这拥有更多属性,但编译器只检查至少所需的属性是否存在并与所需类型匹配。在某些情况下,TypeScript 不会那么宽松,我们稍后会介绍。
我们可以再次编写相同的示例,这次使用接口来描述拥有 label
属性(类型为字符串)的要求。
tsTry
interfaceLabeledValue {label : string;}functionprintLabel (labeledObj :LabeledValue ) {console .log (labeledObj .label );}letmyObj = {size : 10,label : "Size 10 Object" };printLabel (myObj );
接口 LabeledValue
是一个名称,我们现在可以使用它来描述前面示例中的要求。它仍然表示拥有一个名为 label
的属性,类型为 string
。请注意,我们不必像在其他语言中那样明确地说我们传递给 printLabel
的对象实现了这个接口。在这里,只有形状很重要。如果我们传递给函数的对象满足列出的要求,那么它就被允许。
值得指出的是,类型检查器不要求这些属性以任何顺序出现,只要求接口要求的属性存在并具有所需的类型。
可选属性
接口并非所有属性都是必需的。有些属性在特定条件下存在,或者根本不存在。这些可选属性在创建像“选项包”这样的模式时很受欢迎,在这种模式下,您将一个对象传递给一个函数,该函数只填充了几个属性。
以下是一个此模式的示例
tsTry
interfaceSquareConfig {color ?: string;width ?: number;}functioncreateSquare (config :SquareConfig ): {color : string;area : number } {letnewSquare = {color : "white",area : 100 };if (config .color ) {newSquare .color =config .color ;}if (config .width ) {newSquare .area =config .width *config .width ;}returnnewSquare ;}letmySquare =createSquare ({color : "black" });
带有可选属性的接口与其他接口的编写方式类似,每个可选属性在声明中用属性名称末尾的?
表示。
可选属性的优点是,您可以描述这些可能存在的属性,同时仍然可以防止使用不属于接口的属性。例如,如果我们在createSquare
中错误地输入了color
属性的名称,我们会收到一条错误消息,告诉我们
tsTry
interfaceSquareConfig {color ?: string;width ?: number;}functioncreateSquare (config :SquareConfig ): {color : string;area : number } {letnewSquare = {color : "white",area : 100 };if (Property 'clor' does not exist on type 'SquareConfig'. Did you mean 'color'?2551Property 'clor' does not exist on type 'SquareConfig'. Did you mean 'color'?config .) { clor // Error: Property 'clor' does not exist on type 'SquareConfig'Property 'clor' does not exist on type 'SquareConfig'. Did you mean 'color'?2551Property 'clor' does not exist on type 'SquareConfig'. Did you mean 'color'?newSquare .color =config .; clor }if (config .width ) {newSquare .area =config .width *config .width ;}returnnewSquare ;}letmySquare =createSquare ({color : "black" });
只读属性
某些属性应该只在创建对象时可修改。您可以通过在属性名称之前添加readonly
来指定这一点
tsTry
interfacePoint {readonlyx : number;readonlyy : number;}
您可以通过分配对象字面量来构造一个Point
。分配后,x
和y
就不能更改。
tsTry
letp1 :Point = {x : 10,y : 20 };Cannot assign to 'x' because it is a read-only property.2540Cannot assign to 'x' because it is a read-only property.p1 .= 5; // error! x
TypeScript 带有一个ReadonlyArray<T>
类型,它与Array<T>
相同,但删除了所有修改方法,因此您可以确保在创建后不会更改数组
tsTry
leta : number[] = [1, 2, 3, 4];letro :ReadonlyArray <number> =a ;Index signature in type 'readonly number[]' only permits reading.2542Index signature in type 'readonly number[]' only permits reading.ro [0] = 12; // error!Property 'push' does not exist on type 'readonly number[]'.2339Property 'push' does not exist on type 'readonly number[]'.ro .(5); // error! push Cannot assign to 'length' because it is a read-only property.2540Cannot assign to 'length' because it is a read-only property.ro .= 100; // error! length The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.4104The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.= a ro ; // error!
在代码片段的最后一行,您可以看到即使将整个ReadonlyArray
重新分配给一个普通数组也是非法的。但是,您仍然可以使用类型断言来覆盖它
tsTry
leta : number[] = [1, 2, 3, 4];letro :ReadonlyArray <number> =a ;a =ro as number[];
readonly
vs const
记住何时使用readonly
或const
的最简单方法是询问您是在变量上使用它还是在属性上使用它。变量使用const
,而属性使用readonly
。
多余属性检查
在我们第一个使用接口的例子中,TypeScript 允许我们传递 { size: number; label: string; }
给只期望 { label: string; }
的东西。我们也刚刚学习了可选属性,以及它们在描述所谓的“选项包”时如何有用。
然而,简单地将两者结合起来会导致错误潜入。例如,以我们最后一个使用 createSquare
的例子为例
tsTry
interfaceSquareConfig {color ?: string;width ?: number;}functioncreateSquare (config :SquareConfig ): {color : string;area : number } {return {color :config .color || "red",area :config .width ?config .width *config .width : 20,};}letArgument 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'?mySquare =createSquare ({colour : "red",width : 100 });
注意传递给 createSquare
的参数拼写为 colour
而不是 color
。在纯 JavaScript 中,这种事情会静默失败。
你可能会争辩说这个程序类型正确,因为 width
属性是兼容的,没有 color
属性存在,额外的 colour
属性无关紧要。
然而,TypeScript 认为这段代码中可能存在错误。对象字面量会得到特殊处理,并在将其分配给其他变量或将其作为参数传递时进行多余属性检查。如果对象字面量具有“目标类型”没有的任何属性,你将收到错误
tsTry
letArgument 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'?mySquare =createSquare ({colour : "red",width : 100 });
绕过这些检查实际上非常简单。最简单的方法是使用类型断言
tsTry
letmySquare =createSquare ({width : 100,opacity : 0.5 } asSquareConfig );
然而,一个更好的方法可能是添加一个字符串索引签名,如果你确定对象可以有一些以特殊方式使用的额外属性。如果 SquareConfig
可以具有 color
和 width
属性,具有上述类型,但也可以具有任意数量的其他属性,那么我们可以这样定义它
tsTry
interfaceSquareConfig {color ?: string;width ?: number;[propName : string]: any;}
我们将在稍后讨论索引签名,但在这里我们说 SquareConfig
可以具有任意数量的属性,只要它们不是 color
或 width
,它们的类型就无关紧要。
绕过这些检查的最后一种方法可能有点令人惊讶,那就是将对象分配给另一个变量:由于 squareOptions
不会进行多余属性检查,编译器不会给你错误。
tsTry
letsquareOptions = {colour : "red",width : 100 };letmySquare =createSquare (squareOptions );
上述解决方法只要 squareOptions
和 SquareConfig
之间有共同属性就可以工作。在这个例子中,它是 width
属性。但是,如果变量没有任何共同的对象属性,它将失败。例如
tsTry
letsquareOptions = {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
请记住,对于像上面这样的简单代码,你可能不应该试图“绕过”这些检查。对于具有方法和保存状态的更复杂的对象字面量,你可能需要牢记这些技巧,但大多数多余属性错误实际上是 bug。这意味着,如果你在像选项包这样的东西上遇到多余属性检查问题,你可能需要修改一些类型声明。在这种情况下,如果可以将包含 color
或 colour
属性的对象传递给 createSquare
,则应修复 SquareConfig
的定义以反映这一点。
函数类型
接口能够描述 JavaScript 对象可以采用的各种形状。除了描述具有属性的对象之外,接口还能够描述函数类型。
要使用接口描述函数类型,我们给接口一个调用签名。这就像一个函数声明,只给出参数列表和返回类型。参数列表中的每个参数都需要名称和类型。
tsTry
interfaceSearchFunc {(source : string,subString : string): boolean;}
定义后,我们可以像使用其他接口一样使用此函数类型接口。这里,我们展示了如何创建函数类型的变量并为其分配相同类型的函数值。
tsTry
letmySearch :SearchFunc ;mySearch = function (source : string,subString : string): boolean {letresult =source .search (subString );returnresult > -1;};
为了使函数类型正确地进行类型检查,参数的名称不需要匹配。例如,我们可以像这样编写上面的示例
tsTry
letmySearch :SearchFunc ;mySearch = function (src : string,sub : string): boolean {letresult =src .search (sub );returnresult > -1;};
函数参数逐个检查,每个对应参数位置的类型相互检查。如果你不想指定任何类型,TypeScript 的上下文类型推断可以推断参数类型,因为函数值直接分配给 SearchFunc
类型的变量。这里,我们函数表达式的返回类型也由它返回的值(这里是 false
和 true
)隐含。
tsTry
letmySearch :SearchFunc ;mySearch = function (src ,sub ) {letresult =src .search (sub );returnresult > -1;};
如果函数表达式返回的是数字或字符串,类型检查器会报错,提示返回值类型与SearchFunc
接口中描述的返回值类型不匹配。
tsTry
letmySearch :SearchFunc ;Type '(src: string, sub: string) => string' is not assignable to type 'SearchFunc'. Type 'string' is not assignable to type 'boolean'.2322Type '(src: string, sub: string) => string' is not assignable to type 'SearchFunc'. Type 'string' is not assignable to type 'boolean'.= function ( mySearch src ,sub ) {letresult =src .search (sub );return "string";};
可索引类型
与使用接口描述函数类型类似,我们也可以描述可以“索引”的类型,例如a[10]
或ageMap["daniel"]
。可索引类型具有一个索引签名,它描述了用于索引对象的类型,以及索引时的相应返回值类型。
让我们举个例子
tsTry
interfaceStringArray {[index : number]: string;}letmyArray :StringArray ;myArray = ["Bob", "Fred"];letmyStr : string =myArray [0];
上面,我们有一个StringArray
接口,它有一个索引签名。此索引签名表明,当用number
索引StringArray
时,它将返回一个string
。
支持四种类型的索引签名:字符串、数字、符号和模板字符串。可以支持多种类型的索引器,但从数字索引器返回的类型必须是字符串索引器返回的类型的子类型。
这是因为当用number
索引时,JavaScript实际上会将其转换为string
,然后再索引到对象中。这意味着用100
(一个number
)索引与用"100"
(一个string
)索引是相同的,因此两者需要保持一致。
tsTry
interfaceAnimal {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
的类型与字符串索引的类型不匹配,类型检查器会报错
tsTry
interfaceNumberDictionary {[index : string]: number;length : number; // ok, length is a numberProperty '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; // error, the type of 'name' is not a subtype of the indexer name }
但是,如果索引签名是属性类型的联合,则不同类型的属性是可以接受的
tsTry
interfaceNumberOrStringDictionary {[index : string]: number | string;length : number; // ok, length is a numbername : string; // ok, name is a string}
最后,您可以将索引签名设为readonly
,以防止对其索引进行赋值
tsTry
interfaceReadonlyStringArray {readonly [index : number]: string;}letmyArray :ReadonlyStringArray = ["Alice", "Bob"];Index signature in type 'ReadonlyStringArray' only permits reading.2542Index signature in type 'ReadonlyStringArray' only permits reading.myArray [2] = "Mallory"; // error!
您无法设置myArray[2]
,因为索引签名是readonly
。
使用模板字符串的可索引类型
模板字符串可以用来表示允许的特定模式,但不一定是全部模式。例如,HTTP 头部对象可能有一组已知的头部,并支持以x-
为前缀的任何自定义定义属性。
tsTry
interfaceHeadersResponse {"content-type": string,date : string,"content-length": string// Permit any property starting with 'x-'.[headerName : `x-${string}`]: string;}functionhandleResponse (r :HeadersResponse ) {// Handle known, and x- prefixedconsttype =r ["content-type"]constpoweredBy =r ["x-powered-by"]// Unknown keys without the prefix raise errorsconstProperty 'origin' does not exist on type 'HeadersResponse'.2339Property 'origin' does not exist on type 'HeadersResponse'.origin =r .origin }
类类型
实现接口
在 C# 和 Java 等语言中,接口最常见的用途之一是显式强制类满足特定契约,这在 TypeScript 中也是可能的。
tsTry
interfaceClockInterface {currentTime :Date ;}classClock implementsClockInterface {currentTime :Date = newDate ();constructor(h : number,m : number) {}}
您也可以在接口中描述在类中实现的方法,就像我们在下面的示例中对setTime
所做的那样。
tsTry
interfaceClockInterface {currentTime :Date ;setTime (d :Date ): void;}classClock implementsClockInterface {currentTime :Date = newDate ();setTime (d :Date ) {this.currentTime =d ;}constructor(h : number,m : number) {}}
接口描述了类的公共部分,而不是公共和私有部分。这禁止您使用它们来检查类是否也为类实例的私有部分具有特定类型。
类静态和实例部分之间的区别
在处理类和接口时,记住一个类有两种类型很有帮助:静态侧的类型和实例侧的类型。您可能会注意到,如果您创建一个带有构造函数签名的接口,并尝试创建一个实现此接口的类,您会收到错误。
tsTry
interfaceClockConstructor {new (hour : number,minute : number);}classClass 'Clock' incorrectly implements interface 'ClockConstructor'. Type 'Clock' provides no match for the signature 'new (hour: number, minute: number): any'.2420Class 'Clock' incorrectly implements interface 'ClockConstructor'. Type 'Clock' provides no match for the signature 'new (hour: number, minute: number): any'.implements Clock ClockConstructor {currentTime :Date ;constructor(h : number,m : number) {}}
这是因为当一个类实现一个接口时,只检查类的实例侧。由于构造函数位于静态侧,因此不包含在此检查中。
相反,您需要直接使用类的静态侧。在本例中,我们定义了两个接口,ClockConstructor
用于构造函数,ClockInterface
用于实例方法。然后,为了方便起见,我们定义了一个构造函数 createClock
,它创建传递给它的类型的实例。
tsTry
interfaceClockConstructor {new (hour : number,minute : number):ClockInterface ;}interfaceClockInterface {tick (): void;}functioncreateClock (ctor :ClockConstructor ,hour : number,minute : number):ClockInterface {return newctor (hour ,minute );}classDigitalClock implementsClockInterface {constructor(h : number,m : number) {}tick () {console .log ("beep beep");}}classAnalogClock implementsClockInterface {constructor(h : number,m : number) {}tick () {console .log ("tick tock");}}letdigital =createClock (DigitalClock , 12, 17);letanalog =createClock (AnalogClock , 7, 32);
因为 createClock
的第一个参数是 ClockConstructor
类型,所以在 createClock(AnalogClock, 7, 32)
中,它会检查 AnalogClock
是否具有正确的构造函数签名。
另一种简单的方法是使用类表达式。
tsTry
interfaceClockConstructor {new (hour : number,minute : number):ClockInterface ;}interfaceClockInterface {tick (): void;}constClock :ClockConstructor = classClock implementsClockInterface {constructor(h : number,m : number) {}tick () {console .log ("beep beep");}};letclock = newClock (12, 17);clock .tick ();
扩展接口
与类一样,接口可以相互扩展。这允许您将一个接口的成员复制到另一个接口中,这为您在如何将接口分离成可重用组件方面提供了更大的灵活性。
tsTry
interfaceShape {color : string;}interfaceSquare extendsShape {sideLength : number;}letsquare = {} asSquare ;square .color = "blue";square .sideLength = 10;
一个接口可以扩展多个接口,创建一个所有接口的组合。
tsTry
interfaceShape {color : string;}interfacePenStroke {penWidth : number;}interfaceSquare extendsShape ,PenStroke {sideLength : number;}letsquare = {} asSquare ;square .color = "blue";square .sideLength = 10;square .penWidth = 5.0;
混合类型
正如我们之前提到的,接口可以描述现实世界 JavaScript 中存在的丰富类型。由于 JavaScript 的动态性和灵活性,您可能会偶尔遇到一个对象,它作为上面描述的一些类型的组合。
一个例子是既充当函数又充当对象的物体,它还具有额外的属性。
tsTry
interfaceCounter {(start : number): string;interval : number;reset (): void;}functiongetCounter ():Counter {letcounter = function (start : number) {} asCounter ;counter .interval = 123;counter .reset = function () {};returncounter ;}letc =getCounter ();c (10);c .reset ();c .interval = 5.0;
在与第三方 JavaScript 交互时,您可能需要使用上述模式来完整描述类型的形状。
接口扩展类
当接口类型扩展类类型时,它会继承类的成员,但不会继承它们的实现。就好像接口声明了类中的所有成员,但没有提供实现一样。接口甚至继承基类的私有和受保护成员。这意味着,当您创建一个扩展具有私有或受保护成员的类的接口时,该接口类型只能由该类或其子类实现。
当您有一个大型继承层次结构,但又想指定您的代码仅适用于具有某些属性的子类时,这很有用。除了继承自基类之外,子类不必相关。例如
tsTry
classControl {privatestate : any;}interfaceSelectableControl extendsControl {select (): void;}classButton extendsControl implementsSelectableControl {select () {}}classTextBox extendsControl {select () {}}classClass 'ImageControl' incorrectly implements interface 'SelectableControl'. Types have separate declarations of a private property 'state'.2420Class 'ImageControl' incorrectly implements interface 'SelectableControl'. Types have separate declarations of a private property 'state'.implements ImageControl SelectableControl {privatestate : any;select () {}}
在上面的示例中,SelectableControl
包含 Control
的所有成员,包括私有 state
属性。由于 state
是私有成员,因此只有 Control
的后代才能实现 SelectableControl
。这是因为只有 Control
的后代才会有一个源自相同声明的 state
私有成员,这是私有成员兼容的必要条件。
在 Control
类中,可以通过 SelectableControl
的实例访问 state
私有成员。实际上,SelectableControl
就像一个已知具有 select
方法的 Control
。Button
和 TextBox
类是 SelectableControl
的子类型(因为它们都继承自 Control
并具有 select
方法)。ImageControl
类有自己的 state
私有成员,而不是扩展 Control
,因此它不能实现 SelectableControl
。