Interfaces

接口

介绍

TypeScript的核心原则之一是类型检查关注价值的形状。这有时被称为“鸭子打字”或“结构分型”。在TypeScript中,接口充当了命名这些类型的角色,并且是在代码中定义合同以及与代码在项目之外签约的强大方式。

我们的第一界面

查看接口如何工作的最简单方法是从一个简单的例子开始:

function printLabel(labelledObj: { label: string }) { console.log(labelledObj.label } let myObj = {size: 10, label: "Size 10 Object"}; printLabel(myObj

类型检查器检查呼叫printLabel。该printLabel函数有一个参数,它要求传入的对象具有一个名为labelstring类型的属性。请注意,我们的对象实际上具有比此更多的属性,但编译器仅检查至少所需的对象是否存在并匹配所需的类型。在某些情况下,TypeScript不够宽松,我们将稍微介绍一下。

我们可以再次编写相同的示例,这次使用接口来描述使label属性为字符串的要求:

interface LabelledValue { label: string; } function printLabel(labelledObj: LabelledValue) { console.log(labelledObj.label } let myObj = {size: 10, label: "Size 10 Object"}; printLabel(myObj

界面LabelledValue是我们现在可以用来描述前面例子中的需求的名称。它仍然表示有一个名为labelstring的属性。注意我们没有必要明确地说我们传递的对象printLabel实现了这个接口,就像我们在其他语言中可能需要的一样。在这里,这只是重要的形状。如果我们传递给函数的对象符合列出的要求,那么它是允许的。

值得指出的是,类型检查器不要求这些属性以任何顺序出现,只要界面要求的属性存在并具有所需的类型即可。

可选属性

并非所有接口的属性都可能是必需的。有些在某些条件下存在或根本不存在。这些可选属性在创建诸如“选项袋”之类的模式时很受欢迎,在这种模式中,您将对象传递给仅填充了几个属性的函数。

以下是这种模式的一个例子:

interface SquareConfig { color?: string; width?: number; } function createSquare(config: SquareConfig): {color: string; area: number} { let newSquare = {color: "white", area: 100}; if (config.color) { newSquare.color = config.color; } if (config.width) { newSquare.area = config.width * config.width; } return newSquare; } let mySquare = createSquare{color: "black"}

具有可选属性的接口的写法与其他接口类似,每个可选属性由?声明中属性名称末尾的a表示。

可选属性的优点是可以描述这些可能的属性,同时还可以防止使用不属于接口一部分的属性。例如,如果我们错误输入了该color房产的名称createSquare,我们会收到一条错误消息,告知我们:

interface SquareConfig { color?: string; width?: number; } function createSquare(config: SquareConfig): { color: string; area: number } { let newSquare = {color: "white", area: 100}; if (config.color) { // Error: Property 'clor' does not exist on type 'SquareConfig' newSquare.color = config.clor; } if (config.width) { newSquare.area = config.width * config.width; } return newSquare; } let mySquare = createSquare{color: "black"}

只读属性

一些属性只能在首次创建对象时修改。您可以通过readonly在属性名称前面加上来指定它:

interface Point { readonly x: number; readonly y: number; }

你可以Point通过分配一个对象文字来构造一个。转让后,x并且y不能更改。

let p1: Point = { x: 10, y: 20 }; p1.x = 5; // error!

TypeScript附带的ReadonlyArray<T>类型Array<T>与所有删除的变异方法相同,因此您可以确保在创建之后不更改数组:

let a: number[] = [1, 2, 3, 4]; let ro: ReadonlyArray<number> = a; ro[0] = 12; // error! ro.push(5 // error! ro.length = 100; // error! a = ro; // error!

在代码片段的最后一行,您可以看到,即使将整个ReadonlyArray背面分配到普通数组也是非法的。不过,您仍然可以用类型断言覆盖它。

a = ro as number[];

readonly VS const

要记住是使用readonly还是const最简单的方法是询问您是使用变量还是属性。变量使用,const而属性使用readonly

多余的属性检查

在我们的第一个使用接口的例子中,TypeScript让我们传递{ size: number; label: string; }给只有预期的a的东西{ label: string; }。我们还了解了可选属性,以及它们在描述所谓的“选项包”时的用途。

但是,将这两者天真地结合起来就可以让你像使用JavaScript一样在脚下拍摄自己。例如,用我们的最后一个例子createSquare

interface SquareConfig { color?: string; width?: number; } function createSquare(config: SquareConfig): { color: string; area: number } { // ... } let mySquare = createSquare{ colour: "red", width: 100 }

注意给出的参数createSquare拼写colour而不是color。在普通的JavaScript中,这种事情默默无闻。

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

然而,TypeScript认为这个代码中可能存在一个错误。对象字面值得到特殊处理,并在将它们分配给其他变量或将它们作为参数传递时进行过度属性检查。如果对象文字具有“目标类型”不具有的任何属性,则会出现错误。

// error: 'colour' not expected in type 'SquareConfig' let mySquare = createSquare{ colour: "red", width: 100 }

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

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

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

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

我们将稍微讨论一下索引签名,但在这里我们说一个SquareConfig可以有任意数量的属性,只要它们不是color或者width,它们的类型都不重要。

解决这些检查的最后一种方法可能有点令人惊讶,那就是将对象分配给另一个变量:由于squareOptions不会进行额外的属性检查,编译器不会给你一个错误。

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

请记住,对于上面的简单代码,您可能不应该试图“绕过”这些检查。对于具有方法和保持状态的更复杂的对象文字,您可能需要记住这些技巧,但大多数超额属性错误实际上是错误。这意味着,如果您正在运行多个属性检查选项包等问题,则可能需要修改一些类型声明。在这种情况下,如果可以传递具有a colorcolourproperty属性的对象createSquare,则应该修改定义SquareConfig以反映该属性。

函数类型

接口能够描述JavaScript对象可以采用的各种形状。除了描述具有属性的对象之外,接口还能够描述函数类型。

为了用接口描述一个函数类型,我们给这个接口一个呼叫签名。这就像一个只有参数列表和返回类型的函数声明。参数列表中的每个参数都需要名称和类型。

interface SearchFunc { (source: string, subString: string): boolean; }

一旦定义,我们可以像使用其他接口一样使用此函数类型接口。在这里,我们演示如何创建一个函数类型的变量并为其分配一个相同类型的函数值。

let mySearch: SearchFunc; mySearch = function(source: string, subString: string) { let result = source.search(subString return result > -1; }

要使函数类型正确地进行类型检查,参数的名称不需要匹配。例如,我们可以这样写上面的例子:

let mySearch: SearchFunc; mySearch = function(src: string, sub: string): boolean { let result = src.search(sub return result > -1; }

每次检查一个功能参数,每个对应参数位置的类型相互检查。如果您根本不想指定类型,TypeScript的上下文类型可以推断参数类型,因为函数值会直接分配给类型的变量SearchFunc。在这里,我们的函数表达式的返回类型也被它返回的值(这里falsetrue)所暗示。如果函数表达式返回数字或字符串,类型检查器会警告我们返回类型与SearchFunc接口中描述的返回类型不匹配。

let mySearch: SearchFunc; mySearch = function(src, sub) { let result = src.search(sub return result > -1; }

可转位类型

与我们如何使用接口来描述函数类型类似,我们也可以描述我们可以“索引”到的类型a[10],或者ageMap["daniel"]。可索引类型有一个索引签名,它描述了我们可以用来索引对象的类型,以及索引时的相应返回类型。我们举个例子:

interface StringArray { [index: number]: string; } let myArray: StringArray; myArray = ["Bob", "Fred"]; let myStr: string = myArray[0];

上面,我们有一个StringArray有索引签名的接口。这个索引签名指出,当一个StringArray被索引为a时number,它将返回一个string

有两种支持的索引签名:字符串和数字。可以同时支持这两种类型的索引器,但从数字索引器返回的类型必须是从索引器返回的类型的子类型。这是因为使用numberJavaScript进行索引时,JavaScript实际上会将其转换为string索引前的对象。这意味着,与索引100number)是同样的事情与索引"100"string),所以两者必须一致。

class Animal { name: string; } class Dog extends Animal { breed: string; } // Error: indexing with a 'string' will sometimes get you an Animal! interface NotOkay { [x: number]: Animal; [x: string]: Dog; }

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

interface NumberDictionary { [index: string]: number; length: number; // ok, length is a number name: string; // error, the type of 'name' is not a subtype of the indexer }

最后,您可以只读索引签名以防止分配给它们的索引:

interface ReadonlyStringArray { readonly [index: number]: string; } let myArray: ReadonlyStringArray = ["Alice", "Bob"]; myArray[2] = "Mallory"; // error!

您无法设置,myArray[2]因为索引签名是只读的。

类类型

实现一个接口

在TypeScript中,接口在像C#和Java这样的语言中最常见的用法之一就是明确强制类满足特定的合约。

interface ClockInterface { currentTime: Date; } class Clock implements ClockInterface { currentTime: Date; constructor(h: number, m: number) { } }

您也可以在类中实现的接口中描述方法,就像我们setTime在下面的例子中所做的那样:

interface ClockInterface { currentTime: Date; setTime(d: Date } class Clock implements ClockInterface { currentTime: Date; setTime(d: Date) { this.currentTime = d; } constructor(h: number, m: number) { } }

接口描述了课堂的公共方面,而不是公共和私人方面。这禁止您使用它们来检查类是否具有特定类型的类实例的私有方。

类的静态和实例之间的区别

使用类和接口时,记住一个类有两种类型:静态端的类型和实例端的类型。您可能注意到,如果您使用构造签名创建接口并尝试创建实现此接口的类,则会出现错误:

interface ClockConstructor { new (hour: number, minute: number } class Clock implements ClockConstructor { currentTime: Date; constructor(h: number, m: number) { } }

这是因为当一个类实现一个接口时,只会检查该类的实例端。由于构造函数位于静态方面,因此不包含在此检查中。

相反,你需要直接使用类的静态方面。在这个例子中,我们ClockConstructor为构造函数和ClockInterface实例方法定义了两个接口。然后为了方便起见,我们定义一个构造函数createClock来创建传递给它的类型的实例。

interface ClockConstructor { new (hour: number, minute: number): ClockInterface; } interface ClockInterface { tick( } function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface { return new ctor(hour, minute } class DigitalClock implements ClockInterface { constructor(h: number, m: number) { } tick() { console.log("beep beep" } } class AnalogClock implements ClockInterface { constructor(h: number, m: number) { } tick() { console.log("tick tock" } } let digital = createClock(DigitalClock, 12, 17 let analog = createClock(AnalogClock, 7, 32

因为createClock第一个参数是type ClockConstructorcreateClock(AnalogClock, 7, 32),所以它会检查是否AnalogClock有正确的构造函数签名。

扩展接口

像类一样,接口可以相互扩展。这允许您将一个接口的成员复制到另一个接口的成员,这使您可以更灵活地将接口分成多个可重用组件。

interface Shape { color: string; } interface Square extends Shape { sideLength: number; } let square = <Square>{}; square.color = "blue"; square.sideLength = 10;

一个接口可以扩展多个接口,创建所有接口的组合。

interface Shape { color: string; } interface PenStroke { penWidth: number; } interface Square extends Shape, PenStroke { sideLength: number; } let square = <Square>{}; square.color = "blue"; square.sideLength = 10; square.penWidth = 5.0;

混合类型

正如我们前面提到的,接口可以描述现实世界JavaScript中丰富的类型。由于JavaScript的动态性和灵活性,您可能偶尔会遇到一个可以像上述某些类型组合的对象。

一个这样的例子是一个充当函数和对象的对象,具有附加属性:

interface Counter { (start: number): string; interval: number; reset(): void; } function getCounter(): Counter { let counter = <Counter>function (start: number) { }; counter.interval = 123; counter.reset = function () { }; return counter; } let c = getCounter( c(10 c.reset( c.interval = 5.0;

在与第三方JavaScript交互时,您可能需要使用类似上述的模式来完整描述类型的形状。

接口扩展类

当接口类型扩展类类型时,它会继承类的成员,但不会继承它们的实现。就好像界面已经声明了该类的所有成员而没有提供实现。接口甚至可以继承基类的私有和受保护的成员。这意味着当你创建一个用私有或受保护成员扩展类的接口时,该接口类型只能由该类或其子类实现。

当你有一个很大的继承层次结构时,这很有用,但是要指定你的代码只与具有某些属性的子类一起工作。除了从基类继承之外,子类不必是相关的。例如:

class Control { private state: any; } interface SelectableControl extends Control { select(): void; } class Button extends Control implements SelectableControl { select() { } } class TextBox extends Control { } // Error: Property 'state' is missing in type 'Image'. class Image implements SelectableControl { select() { } } class Location { }

在上面的例子中,SelectableControl包含所有的成员Control,包括私有state财产。既然state是私人成员,只有后裔才有可能Control实施SelectableControl。这是因为只有后代Control会有一个state私人会员才能发起同样的声明,这是私人会员必须兼容的要求。

Control类别内,可以state通过一个实例访问私有成员SelectableControl。实际上,这样的SelectableControl行为Control已知有一种select方法。在ButtonTextBox类的亚型SelectableControl(因为它们都继承Control并有select方法),但ImageLocation类都没有。