接口
TypeScript的一个核心原则是类型检测重点放在值的形状(shape),这有时候被称为鸭子类型化(duck typing)或结构子类型化(structural subtyping)。在TypeScript中,用接口(interfaces)来命名这些类型,来定义项目内部代码的合约以及与外部代码的契约。
第一个接口
理解interface如何工作,最容易的方式就是先看一个简单例子:
function printLabel(labelledObj: {label: string}) { console.log(labelledObj.label); } var myObj = {size: 10, label: "Size 10 Object"}; printLabel(myObj);
当调用‘printLabel‘时类型检测器开始检查,‘printLabel‘函数有单个参数,要求传入的对象有一个类型为string,名为‘label‘的属性。注意这里传入的对象有多个属性,但编译器仅检测所需要的属性存在而且类型匹配即可。
可以重写上面的例子,但这次是用接口来描述要有一个类型为string,名为‘label‘的property:
interface LabelledValue { label: string; } function printLabel(labelledObj: LabelledValue) { console.log(labelledObj.label); } var myObj = {size: 10, label: "Size 10 Object"}; printLabel(myObj);
interface ‘LabelledValue‘是描述前一个例子所需要的一个名字,它仍然表示要有一个类型为string,名为‘label‘的属性。注意不必明确地给将这个接口的实现传递给‘printLabel‘,这个与其他语言类似。这里重要的只是形状(shape)。如果传递给函数的对象满足列出的需求,那么就允许传入。
需要指出的是类型检测器不需要这些属性按照某种方式排序,只要接口所需的属性存在且类型匹配即可通过检测。
可选属性
并非需要一个接口中所有的属性(properties)。只有在特定条件下一些属性才存在,或者并非存在所有的属性。当创建类似于"option bags"模式时,可选属性很普遍,传递给函数的对象只有部分属性被赋值。
下面是该模式的一个例子:
interface SquareConfig { color?: string; width?: number; } function createSquare(config: SquareConfig): {color: string; area: number} { var newSquare = {color: "white", area: 100}; if (config.color) { newSquare.color = config.color; } if (config.width) { newSquare.area = config.width * config.width; } return newSquare; } var mySquare = createSquare({color: "black"});
有可选属性的接口在编码上与其他接口类似,每个可选属性在属性声明时用一个 ‘?‘来标记。
可选属性的优点是可以描述可能存在的属性,同时对那些未填充的属性也会做类型检测。例如假定传递给‘createSquare‘的属性名称拼写错误,则会得到下面错误消息:
interface SquareConfig { color?: string; width?: number; } function createSquare(config: SquareConfig): {color: string; area: number} { var newSquare = {color: "white", area: 100}; if (config.color) { newSquare.color = config.collor; // Type-checker can catch the mistyped name here } if (config.width) { newSquare.area = config.width * config.width; } return newSquare; } var mySquare = createSquare({color: "black"});
函数类型
接口可以描述JavaScript对象可以接受的各种各样的形状(Shape)。 除了描述带有属性的对象,接口还可以描述函数类型。
为了用接口描述函数类型,给接口一个调用标记(call signature),类似于只给出参数列表和返回值的一个函数声明。
interface SearchFunc { (source: string, subString: string): boolean; }
一旦定义,就可以像其他接口一样来使用该函数类型接口。下面展示如何创建一个函数类型变量,将相同类型的一个函数值赋值给它。
var mySearch: SearchFunc; mySearch = function(source: string, subString: string) { var result = source.search(subString); if (result == -1) { return false; } else { return true; } }
函数类型要能够通过类型检测,不需要参数名称保持一致。可以将上面的例子写为:
var mySearch: SearchFunc; mySearch = function(src: string, sub: string) { var result = src.search(sub); if (result == -1) { return false; } else { return true; } }
函数参数被依次一个一个检测,检测每个参数位置对应的类型是否匹配。而且这里函数表达式的返回类型已经由返回值(false与true)暗示出。如果函数表达式返回的是numbers或strings,那么类型检测器将告警:返回类型与SearchFunc接口描述的返回类型不匹配。
数组类型
类似于如何利用接口来描述函数类型,接口也可以描述数组类型。数组类型有一个描述对象索引的‘index‘类型,以及访问索引对应的返回类型。
interface StringArray { [index: number]: string;} var myArray: StringArray;myArray = ["Bob", "Fred"];
index可以有两种类型:string和number。可以同时支持两种index类型,但要求从numeric index返回的类型必须是从string index返回类型的子类型。
index标记功能的强大在于可描述数组和字典模式,还要求属性都要匹配索引返回类型。在下面例子中,属性没有匹配索引返回类型,因此类型检测器给出错误:
interface Dictionary { [index: string]: string; length: number; // error, the type of ‘length‘ is not a subtype of the indexer}
Class类型
实现接口
在C#和Java等语言中接口最常见的一个用途是,明确强制类需要满足一个特定的契约,在TypeScript语言中同样适用:
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) { } }
接口描述类的公开(Public)部分,而不包含私有部分。可以据此来检测类中也包含类实例私有部分的数据类型。
类的静态部分与实例部分之间的差异
当使用类与接口时,要注意类有两种类型:静态类型部分与实例类型部分(the type of the static side and the type of the instance side)。如果创建一个有构造函数标记的接口,然后试图创建一个实现该接口的类时将得到错误:
interface ClockInterface { new (hour: number, minute: number); } class Clock implements ClockInterface { currentTime: Date; constructor(h: number, m: number) { } }
这是因为当类实现一个接口时,只检测类的实例部分。由于构造函数是在静态部分,因此实例部分中没有包含构造函数,当检测时就报错。
这时,需要在类中直接实现静态部分。在下面例子中直接使用类来实现静态部分:
interface ClockStatic { new (hour: number, minute: number); } class Clock { currentTime: Date; constructor(h: number, m: number) { } } var cs: ClockStatic = Clock; var newClock = new cs(7, 30);
扩展接口
与类很相似的是interfaces可以扩展。这样就可以将一个接口中的成员拷贝到另一个接口中,因此可以将接口划分为更细的可重用的组件:
interface Shape { color: string; } interface Square extends Shape { sideLength: number; } var square = <Square>{}; square.color = "blue"; square.sideLength = 10;
一个接口可以扩展多个接口,将这些接口组合在一起:
interface Shape { color: string; } interface PenStroke { penWidth: number; } interface Square extends Shape, PenStroke { sideLength: number; } var square = <Square>{}; square.color = "blue"; square.sideLength = 10; square.penWidth = 5.0;
混合类型
前面提到,接口可以描述JavaScript中的许多类型。由于JavaScript语言的动态和灵活性,可能遇到一个对象是上面多个类型的组合体。
在下面例子中的对象包含一个函数类型,一个对象类型,以及一些属性:
interface Counter { (start: number): string; interval: number; reset(): void; } var c: Counter; c(10); c.reset(); c.interval = 5.0;
当与第三方JavaScript交互时,可能会用类似上面的模式来描述一个类型的完整形状(shape)。
翻译后记:
需要学习下 鸭子类型化(duck typing)、结构子类型化(structural subtyping)、"option bags"模式。
参考资料
[1] http://www.typescriptlang.org/Handbook#interfaces
[2] TypeScript - Interfaces, 破狼blog, http://greengerong.com/blog/2014/11/13/typescript-interfaces/
[3] TypeScript系列1-简介及版本新特性, http://my.oschina.net/1pei/blog/493012
[4] TypeScript系列2-手册-基础类型, http://my.oschina.net/1pei/blog/493181