接口

TypeScript的一个核心守则就是专注于检查值的“形状(shape)”。在TypeScript中,接口承担起了这个职责。你可以使用它来命名类型,它是结构化代码的好工具,并且你也可以使用它来对外部调用代码进行约束。

第一个接口

一个简单的接口例子:

  1. function printLabel(labelledObj: {label: string}) {
  2. console.log(labelledObj.label);
  3. }
  4. var myObj = {size: 10, label: "Size 10 Object"};
  5. printLabel(myObj);

TypeScript的类型检查器会去检查printLabel的调用。printLabel有一个参数,且这个参数必须是一个包含一个名为label的字符串属性的对象。值得注意的是,我们实际传入的对象,其实包含了额外的属性,但类型检查器仅仅只会去检查指定的属性是否正确存在。

让我们重构一下以上代码,这次我们使用接口来描述一个必须包含一个名为label的字符串属性的对象:

  1. interface LabelledValue {
  2. label: string;
  3. }
  4. function printLabel(labelledObj: LabelledValue) {
  5. console.log(labelledObj.label);
  6. }
  7. var myObj = {size: 10, label: "Size 10 Object"};
  8. printLabel(myObj);

接口LabelledValue与我们在上一个例子中所做的事是一样的。值得注意的是,我们并不需要像在其他的强类型语言中一样,传入printLabel的参数一定非得是一个实现了这个接口的对象。在TypeScript中,它们只需在形态上一致就可以了。只要我们传递的参数符合接口所列出的定义,它就是合法的。

还有一个值得注意的点是,类型检查器并不会去关注接口中属性在被定义时的顺序。你可以以任意的顺序来实现它。

可选属性

一个接口中的所有属性并不都是必须的。在一些情况下,它们可能并不存在。比如,当用户传递一个option对象参数时,可能有很多配置属性都是可选的。

可选属性例子:

  1. interface SquareConfig {
  2. color?: string;
  3. width?: number;
  4. }
  5. function createSquare(config: SquareConfig): {color: string; area: number} {
  6. var newSquare = {color: "white", area: 100};
  7. if (config.color) {
  8. newSquare.color = config.color;
  9. }
  10. if (config.width) {
  11. newSquare.area = config.width * config.width;
  12. }
  13. return newSquare;
  14. }
  15. var mySquare = createSquare({color: "black"});

可选属性的写法和接口中其他的属性写法很像,仅仅在冒号之前添加一个?即可。

可选属性的优势在于,我们即可以用它来描述可能出现的属性,同时又可以检查出那些不该存在的属性。如下面的例子中,我们在定义createSquare的函数体时,config的一个属性名拼错了,我们将会得到一个报错:

  1. interface SquareConfig {
  2. color?: string;
  3. width?: number;
  4. }
  5. function createSquare(config: SquareConfig): {color: string; area: number} {
  6. var newSquare = {color: "white", area: 100};
  7. if (config.color) {
  8. newSquare.color = config.collor; // Type-checker can catch the mistyped name here
  9. }
  10. if (config.width) {
  11. newSquare.area = config.width * config.width;
  12. }
  13. return newSquare;
  14. }
  15. var mySquare = createSquare({color: "black"});

函数类型

接口除了可以用来定义JavaScript对象中各式各样的属性。它也可以用来定义函数。

在定义函数类型的接口时,语法很像函数声明,但是只有参数列表和返回值类型。

  1. interface SearchFunc {
  2. (source: string, subString: string): boolean;
  3. }

一旦定义了函数接口,我们就可以像普通接口去使用它。下面的例子中,我们定义了一个函数接口,然后定义了一个函数实现了它:

  1. var mySearch: SearchFunc;
  2. mySearch = function(source: string, subString: string) {
  3. var result = source.search(subString);
  4. if (result == -1) {
  5. return false;
  6. }
  7. else {
  8. return true;
  9. }
  10. }

在做函数接口的类型检查时,参数的名字是不必完全一样的,下面的例子也是完全合法的:

  1. var mySearch: SearchFunc;
  2. mySearch = function(src: string, sub: string) {
  3. var result = src.search(sub);
  4. if (result == -1) {
  5. return false;
  6. }
  7. else {
  8. return true;
  9. }
  10. }

除了检查参数之外,函数的返回值也会被检查(这里是truefalse)。如果函数返回数字或字符串,那么类型检查器将会提示我们,函数的返回值与SearchFunc接口中所描述的不一致。

数组类型

既然我们可以用接口来描述函数,我们也可以用接口来描述数组。数组有一个“索引(index)”类型来描述这个数组的索引的类型,紧接着的是对应位置元素的返回值。

  1. interface StringArray {
  2. [index: number]: string;
  3. }
  4. var myArray: StringArray;
  5. myArray = ["Bob", "Fred"];

TypeScript支持两种索引类型:字符串和数字。一个数组同时支持两种索引也是允许的,但是有一个限制,数字索引的返回值的类型,必须是字符串索引的返回值类型的子类型。

尽管索引在描述数组和“字典(dictionary)”时很有用,但是它们也限制了返回值的类型。在下面的例子中,有属性没有符合索引中指定的返回值类型,所以会得到一个报错:

  1. interface Dictionary {
  2. [index: string]: string;
  3. length: number; // error, the type of 'length' is not a subtype of the indexer
  4. }

实现一个接口

接口在C#Java中的一大用处,就是给予一个类明确的限制。在TypeScript中,一样如此:

  1. interface ClockInterface {
  2. currentTime: Date;
  3. }
  4. class Clock implements ClockInterface {
  5. currentTime: Date;
  6. constructor(h: number, m: number) { }
  7. }

你也可以在一个类的接口中定义一些方法,下面的例子中,我们定义了setTime方法:

  1. interface ClockInterface {
  2. currentTime: Date;
  3. setTime(d: Date);
  4. }
  5. class Clock implements ClockInterface {
  6. currentTime: Date;
  7. setTime(d: Date) {
  8. this.currentTime = d;
  9. }
  10. constructor(h: number, m: number) { }
  11. }

接口仅仅用于定义一个类中的公有部分。用接口来定义类的私有属性/方法,都是不允许的。

类中的静态部分和实例部分

在使用类和接口时,你需要记住,一个类包含两部分:静态部分和实例部分。你可能会注意到,当你在一个接口中声明构造函数,并且尝试让一个类去实现这个接口时,你会得到一个错误:

  1. interface ClockInterface {
  2. new (hour: number, minute: number);
  3. }
  4. class Clock implements ClockInterface {
  5. currentTime: Date;
  6. constructor(h: number, m: number) { }
  7. }

这是因为当类实现一个接口时,只有类的实例部分才会被检查。由于构造函数是类的静态部分,它将不会被检查。

所以,你应当直接在类中处理静态部分:

  1. interface ClockStatic {
  2. new (hour: number, minute: number);
  3. }
  4. class Clock {
  5. currentTime: Date;
  6. constructor(h: number, m: number) { }
  7. }
  8. var cs: ClockStatic = Clock;
  9. var newClock = new cs(7, 30);

继承接口

和类一样,接口也可以相互继承。所以,你不需要在接口之间拷贝那些公有的属性了。它使你可以抽出接口之间的可重用部分:

  1. interface Shape {
  2. color: string;
  3. }
  4. interface Square extends Shape {
  5. sideLength: number;
  6. }
  7. var square = <Square>{};
  8. square.color = "blue";
  9. square.sideLength = 10;

一个接口可以继承多个接口:

  1. interface Shape {
  2. color: string;
  3. }
  4. interface PenStroke {
  5. penWidth: number;
  6. }
  7. interface Square extends Shape, PenStroke {
  8. sideLength: number;
  9. }
  10. var square = <Square>{};
  11. square.color = "blue";
  12. square.sideLength = 10;
  13. square.penWidth = 5.0;

混合类型

正如我们之前提到的,接口可以描述JavaScript世界中的许多类型。但是因为JavaScript天生就是动态的,你们可能会遇到一些混合类型的对象。

以下例子是一个即使函数类型又是对象类型的JavaScript对象:

  1. interface Counter {
  2. (start: number): string;
  3. interval: number;
  4. reset(): void;
  5. }
  6. var c: Counter;
  7. c(10);
  8. c.reset();
  9. c.interval = 5.0;

当和第三方JavaScript库打交道时,你可能会用上上述特性,用以完整得描述这些库。