接口
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手册翻译系列1-基础类型, http://my.oschina.net/1pei/blog/493181