typescript中文手册,你想知道的这儿全都有~

基础类型 介绍
为了让程序有价值,我们需要能够处理最简单的数据单元:数字,字符串,结构体,布尔值等。
TypeScript支持与JavaScript几乎相同的数据类型,此外还提供了实用的枚举类型方便我们使用。

布尔值
最基本的数据类型就是简单的true/false值,在JavaScript和TypeScript里叫做boolean(其它语言中也一样)。

var isDone: boolean = false;
1
数字
和JavaScript一样,TypeScript里的所有数字都是浮点数。
这些浮点数的类型是number。
除了支持十进制和十六进制字面量,Typescript还支持ECMAScript 2015中引入的二进制和八进制字面量。

var decLiteral: number = 6;
var hexLiteral: number = 0x9837abdef;
var binaryLiteral: number = 0b0010;
var octalLiteral: number = 0o74563;
1
2
3
4
字符串
JavaScript程序的另一项基本操作是处理网页或服务器端的文本数据。
像其它语言里一样,我们使用string表示文本数据类型。
和JavaScript一样,可以使用双引号(")或单引号(’)表示字符串。

var name: string = “bob”;
name = “smith”;
1
2
你还可以使用模版字符串,它可以定义多行文本和内嵌表达式。
这种字符串是被反引号包围(`),并且以${ expr }这种形式嵌入表达式

var name: string = Gene;
var age: number = 37;
var sentence: string = `Hello, my name is ${ name }.

I’ll be ${ age + 1 } years old next month.`;
1
2
3
4
5
这与下面定义sentence的方式效果相同:

var sentence: string = "Hello, my name is " + name + “.\n\n” +
“I’ll be " + (age + 1) + " years old next month.”;
1
2
数组
TypeScript像JavaScript一样可以操作数组元素。
有两种方式可以定义数组。
第一种,可以在元素类型后面接上[],表示由此类型元素组成的一个数组:

var list: number[] = [1, 2, 3];
1
第二种方式是使用数组泛型,Array<元素类型>:

var list: Array = [1, 2, 3];
1
枚举
enum类型是对JavaScript标准数据类型的一个补充。
像C#等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字。

enum Color {Red, Green, Blue};
var c: Color = Color.Green;
1
2
默认情况下,从0开始为元素编号。
你也可以手动的指定成员的数值。
例如,我们将上面的例子改成从1开始编号:

enum Color {Red = 1, Green, Blue};
var c: Color = Color.Green;
1
2
或者,全部都采用手动赋值:

enum Color {Red = 1, Green = 2, Blue = 4};
var c: Color = Color.Green;
1
2
枚举类型提供的一个便利是你可以由枚举的值得到它的名字。
例如,我们知道数值为2,但是不确定它映射到Color里的哪个名字,我们可以查找相应的名字:

enum Color {Red = 1, Green, Blue};
var colorName: string = Color[2];

alert(colorName);
1
2
3
4
任意值
有时,我们可能会想要为那些在编写程序阶段还不清楚其类型的变量指定一个类型。
这些值可能来自于动态的内容,比如来自用户或第三方代码库。
这种情况下,我们不希望类型检查器对这些值进行检查或者说让它们直接通过编译阶段的检查。
那么我们可以使用any类型来标记这些变量:

var notSure: any = 4;
notSure = “maybe a string instead”;
notSure = false; // okay, definitely a boolean
1
2
3
在对现有代码进行改写的时候,any类型是十分有用的,它允许你在编译时可选择地包含或移除类型检查。
你可能认为Object有差不多的作用,就像它在其它语言中那样。
但是Object类型的变量只是允许你给它赋任意值 – 但是你不像在它上面调用任意方法,就算它真的包含了这些方法:

var notSure: any = 4;
notSure.ifItExists(); // okay, ifItExists might exist at runtime
notSure.toFixed(); // okay, toFixed exists (but the compiler doesn’t check)
var prettySure: Object = 4;
prettySure.toFixed(); // Error: Property ‘toFixed’ doesn’t exist on type ‘Object’.
1
2
3
4
5
当你只知道数据的类型的一部分时,any类型也是有用的。
比如,你有一个数组,它包含了不同的数据类型:

var list: any[] = [1, true, “free”];

list[1] = 100;
1
2
3
空值
某种程度上来说,void类型像是与any类型相反,它表示没有任何类型。
当一个函数没有返回值时,你通常会见到其返回值类型是void:

function warnUser(): void {
alert(“This is my warning message”);
}
1
2
3
声明一个void类型的变量没有什么大用,因为你只能为它赋予undefined和null:

var unusable: void = undefined;
1
接口 介绍
TypeScript的核心原则之一是对值所具有的shape进行类型检查。
它有时被称做“鸭式辨型法”或“结构性子类型化”。
在TypeScript里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。

接口初探
下面通过一个简单示例来观察接口是如何工作的:

function printLabel(labelledObj: { label: string }) {
console.log(labelledObj.label);
}

var myObj = { size: 10, label: “Size 10 Object” };
printLabel(myObj);
1
2
3
4
5
6
类型检查器会查看printLabel的调用。
printLabel有一个参数,并要求这个对象参数有一个名为label类型为string的属性。
需要注意的是,我们传入的对象参数实际上会包含很多属性,但是编译器只会检查那些必需的属性是否存在,并且其类型是否匹配。

下面我们重写上面的例子,这次使用接口来描述:必须包含一个label属性且类型为string:

interface LabelledValue {
label: string;
}

function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}

var myObj = {size: 10, label: “Size 10 Object”};
printLabel(myObj);
1
2
3
4
5
6
7
8
9
10
LabelledValue接口就好比一个名字,用来描述上面例子里的要求。
它代表了有一个label属性且类型为string的对象。
需要注意的是,我们在这里并不能像在其它语言里一样,说传给printLabel的对象实现了这个接口。我们只会去关注值的外形。
只要传入的对象满足上面提到的必要条件,那么它就是被允许的。

还有一点值得提的是,类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以。

可选属性
接口里的属性不全都是必需的。
有些是只在某些条件下存在,或者根本不存在。
可选属性在应用“option bags”模式时很常用,即给函数传入的参数对象中只有部分属性赋值了。

下面是应用了“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”});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个?符号。

可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误。
比如,我们故意将createSquare里的color属性名拼错,就会得到一个错误提示:

interface SquareConfig {
color?: string;
width?: number;
}

function createSquare(config: SquareConfig): {color: string; area: number} {
var newSquare = {color: “white”, area: 100};
if (config.color) {
// Error: Property ‘collor’ does not exist on type ‘SquareConfig’
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”});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
函数类型
接口能够描述JavaScript中对象拥有的各种各样的外形。
除了描述带有属性的普通对象外,接口也可以描述函数类型。

为了使用接口表示函数类型,我们需要给接口定义一个调用签名。
它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。

interface SearchFunc {
(source: string, subString: string): boolean;
}
1
2
3
这样定义后,我们可以像使用其它接口一样使用这个函数类型的接口。
下例展示了如何创建一个函数类型的变量,并将一个同类型的函数赋值给这个变量。

var mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
var result = source.search(subString);
if (result == -1) {
return false;
}
else {
return true;
}
}
1
2
3
4
5
6
7
8
9
10
对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配。
比如,我们使用下面的代码重写上面的例子:

var mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
var result = src.search(sub);
if (result == -1) {
return false;
}
else {
return true;
}
}
1
2
3
4
5
6
7
8
9
10
函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。
如果你不想指定类型,Typescript的类型系统会推断出参数类型,因为函数直接赋值给了SearchFunc类型变量。
函数的返回值类型是通过其返回值推断出来的(此例是false和true)。
如果让这个函数返回数字或字符串,类型检查器会警告我们函数的返回值类型与SearchFunc接口中的定义不匹配。

var mySearch: SearchFunc;
mySearch = function(src, sub) {
var result = src.search(sub);
if (result == -1) {
return false;
}
else {
return true;
}
}
1
2
3
4
5
6
7
8
9
10
数组类型
与使用接口描述函数类型差不多,我们也可以描述数组类型。
数组类型具有一个index类型表示索引的类型,还有一个相应的返回值类型表示通过索引得到的元素的类型。

interface StringArray {
[index: number]: string;
}

var myArray: StringArray;
myArray = [“Bob”, “Fred”];
1
2
3
4
5
6
支持两种索引类型:string和number。
数组可以同时使用这两种索引类型,但是有一个限制,数字索引返回值的类型必须是字符串索引返回值的类型的子类型。

索引签名能够很好的描述数组和dictionary模式,它们也要求所有属性要与返回值类型相匹配。
因为字符串索引表明obj.property和obj[“property”]两种形式都可以。
下面的例子里,length的类型与字符串索引类型不匹配,所以类型检查器给出一个错误提示:

interface NumberDictionary {
[index: string]: number;
length: number; // 可以,length是number类型
name: string // 错误,name的类型不是索引类型的子类型
}
1
2
3
4
5
类类型
实现接口
与C#或Java里接口的基本作用一样,TypeScript也能够用它来明确的强制一个类去符合某种契约。

interface ClockInterface {
currentTime: Date;
}

class Clock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number) { }
}
1
2
3
4
5
6
7
8
你也可以在接口中描述一个方法,在类里实现它,如同下面的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) { }
}
1
2
3
4
5
6
7
8
9
10
11
12
接口描述了类的公共部分,而不是公共和私有两部分。
它不会帮你检查类是否具有某些私有成员。

类静态部分与实例部分的区别
当你操作类和接口的时候,你要知道类是具有两个类型的:静态部分的类型和实例的类型。
你会注意到,当你用构造器签名去定义一个接口并试图定义一个类去实现这个接口时会得到一个错误:

interface ClockConstructor {
new (hour: number, minute: number);
}

class Clock implements ClockConstructor {
currentTime: Date;
constructor(h: number, m: number) { }
}
1
2
3
4
5
6
7
8
这里因为当一个类实现了一个接口时,只对其实例部分进行类型检查。
constructor存在于类的静态部分,所以不在检查的范围内。

因此,我们应该直接操作类的静态部分。
看下面的例子,我们定义了两个接口,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”);
}
}

var digital = createClock(DigitalClock, 12, 17);
var analog = createClock(AnalogClock, 7, 32);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
因为createClock的第一个参数是ClockConstructor类型,在createClock(AnalogClock, 12, 17)里,会检查AnalogClock是否符合构造函数签名。

扩展接口
和类一样,接口也可以相互扩展。
这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。

interface Shape {
color: string;
}

interface Square extends Shape {
sideLength: number;
}

var square = {};
square.color = “blue”;
square.sideLength = 10;
1
2
3
4
5
6
7
8
9
10
11
一个接口可以继承多个接口,创建出多个接口的合成接口。

interface Shape {
color: string;
}

interface PenStroke {
penWidth: number;
}

interface Square extends Shape, PenStroke {
sideLength: number;
}

var square = {};
square.color = “blue”;
square.sideLength = 10;
square.penWidth = 5.0;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
混合类型
先前我们提过,接口能够描述JavaScript里丰富的类型。
因为JavaScript其动态灵活的特点,有时你会希望一个对象可以同时具有上面提到的多种类型。

一个例子就是,一个对象可以同时做为函数和对象使用,并带有额外的属性。

interface Counter {
(start: number): string;
interval: number;
reset(): void;
}

var c: Counter;
c(10);
c.reset();
c.interval = 5.0;
1
2
3
4
5
6
7
8
9
10
在使用JavaScript第三方库的时候,你可能需要像上面那样去完整地定义类型。

接口继承类
当接口继承了一个类类型时,它会继承类的成员但不包括其实现。
就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。
接口同样会继承到类的private和protected成员。
这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)。

这是很有用的,当你有一个很深层次的继承,但是只想你的代码只是针对拥有特定属性的子类起作用的时候。子类除了继承自基类外与基类没有任何联系。
例:

class Control {
private state: any;
}

interface SelectableControl extends Control {
select(): void;
}

class Button extends Control {
select() { }
}
class TextBox extends Control {
select() { }
}
class Image extends Control {
}
class Location {
select() { }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在上面的例子里,SelectableControl包含了Control的所有成员,包括私有成员state。
因为state是私有成员,所以只能够是Control的子类们才能实现SelectableControl接口。
因为只有Control的子类才能够拥有一个声明于Control的私有成员state,这对私有成员的兼容性是必需的。

在Control类内部,是允许通过SelectableControl的实例来访问私有成员state的。
实际上,SelectableControl就像Control一样,并拥有一个select方法。
Button和TextBox类是SelectableControl的子类(类为它们都继承自Control并有select方法),但Image和Location类并不是这样的。

类 介绍
传统的JavaScript程序使用函数和基于原型的继承来创建可重用的组件,但这对于熟悉使用面向对象方式的程序员来说有些棘手,因为他们用的是基于类的继承并且对象是从类构建出来的。
从ECMAScript 2015,也就是ECMAScript 6,JavaScript程序将可以使用这种基于类的面向对象方法。
在TypeScript里,我们允许开发者现在就使用这些特性,并且编译后的JavaScript可以在所有主流浏览器和平台上运行,而不需要等到下个JavaScript版本。


下面看一个使用类的例子:

class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}

var greeter = new Greeter(“world”);
1
2
3
4
5
6
7
8
9
10
11
如果你使用过C#或Java,你会对这种语法非常熟悉。
我们声明一个Greeter类。这个类有3个成员:一个叫做greeting的属性,一个构造函数和一个greet方法。

你会注意到,我们在引用任何一个类成员的时候都用了this。
它表示我们访问的是类的成员。

最后一行,我们使用new构造了Greeter类的一个实例。
它会调用之前定义的构造函数,创建一个Greeter类型的新对象,并执行构造函数初始化它。

继承
在TypeScript里,我们可以使用常用的面向对象模式。
当然,基于类的程序设计中最基本的模式是允许使用继承来扩展一个类。

看下面的例子:

class Animal {
name:string;
constructor(theName: string) { this.name = theName; }
move(distanceInMeters: number = 0) {
console.log(${this.name} moved ${distanceInMeters}m.);
}
}

class Snake extends Animal {
constructor(name: string) { super(name); }
move(distanceInMeters = 5) {
console.log(“Slithering…”);
super.move(distanceInMeters);
}
}

class Horse extends Animal {
constructor(name: string) { super(name); }
move(distanceInMeters = 45) {
console.log(“Galloping…”);
super.move(distanceInMeters);
}
}

var sam = new Snake(“Sammy the Python”);
var tom: Animal = new Horse(“Tommy the Palomino”);

sam.move();
tom.move(34);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
这个例子展示了TypeScript中继承的一些特征,与其它语言类似。
我们使用extends来创建子类。你可以看到Horse和Snake类是基类Animal的子类,并且可以访问其属性和方法。

这个例子演示了如何在子类里可以重写父类的方法。
Snake类和Horse类都创建了move方法,重写了从Animal继承来的move方法,使得move方法根据不同的类而具有不同的功能。
注意,即使tom被声明为Animal类型,因为它的值是Horse,tom.move(34)调用Horse里的重写方法:

Slithering…
Sammy the Python moved 5m.
Galloping…
Tommy the Palomino moved 34m.
1
2
3
4
公共,私有与受保护的修饰符
默认为公有
在上面的例子里,我们可以自由的访问程序里定义的成员。
如果你对其它语言中的类比较了解,就会注意到我们在之前的代码里并没有使用public来做修饰;例如,C#要求必须明确地使用public指定成员是可见的。
在TypeScript里,每个成员默认为public的。

你也可以明确的将一个成员标记成public。
我们可以用下面的方式来重写上面的Animal类:

class Animal {
public name: string;
public constructor(theName: string) { this.name = theName; }
move(distanceInMeters: number) {
console.log(${this.name} moved ${distanceInMeters}m.);
}
}
1
2
3
4
5
6
7
理解private
当成员被标记成private时,它就不能在声明它的类的外部访问。比如:

class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}

new Animal(“Cat”).name; // Error: ‘name’ is private;
1
2
3
4
5
6
TypeScript使用的是结构性类型系统。
当我们比较两种不同的类型时,并不在乎它们从哪儿来的,如果所有成员的类型都是兼容的,我们就认为它们的类型是兼容的。

然而,当我们比较带有private或protected成员的类型的时候,情况就不同了。
如果其中一个类型里包含一个private成员,那么只有当另外一个类型中也存在这样一个private成员, 并且它们是来自同一处声明时,我们才认为这两个类型是兼容的。
对于protected成员也使用这个规则。

下面来看一个例子,详细的解释了这点:

class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}

class Rhino extends Animal {
constructor() { super(“Rhino”); }
}

class Employee {
private name: string;
constructor(theName: string) { this.name = theName; }
}

var animal = new Animal(“Goat”);
var rhino = new Rhino();
var employee = new Employee(“Bob”);

animal = rhino;
animal = employee; // Error: Animal and Employee are not compatible
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
这个例子中有Animal和Rhino两个类,Rhino是Animal类的子类。
还有一个Employee类,其类型看上去与Animal是相同的。
我们创建了几个这些类的实例,并相互赋值来看看会发生什么。
因为Animal和Rhino共享了来自Animal里的私有成员定义private name: string,因此它们是兼容的。
然而Employee却不是这样。当把Employee赋值给Animal的时候,得到一个错误,说它们的类型不兼容。
尽管Employee里也有一个私有成员name,但它明显不是Animal里面定义的那个。

理解protected
protected修饰符与private修饰符的行为很相似,但有一点不同,protected成员在派生类中仍然可以访问。例如:

class Person {
protected name: string;
constructor(name: string) { this.name = name; }
}

class Employee extends Person {
private department: string;

constructor(name: string, department: string) {
    super(name)
    this.department = department;
}

public getElevatorPitch() {
    return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}

}

var howard = new Employee(“Howard”, “Sales”);
console.log(howard.getElevatorPitch());
console.log(howard.name); // error
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
注意,我们不能在Person类外使用name,但是我们仍然可以通过Employee类的实例方法访问,因为Employee是由Person派生出来的。

参数属性
在上面的例子中,我们不得不定义一个私有成员name和一个构造函数参数theName,并且立刻给name和theName赋值。
这种情况经常会遇到。参数属性可以方便地让我们在一个地方定义并初始化一个成员。
下面的例子是对之前Animal类的修改版,使用了参数属性:

class Animal {
constructor(private name: string) { }
move(distanceInMeters: number) {
console.log(${this.name} moved ${distanceInMeters}m.);
}
}
1
2
3
4
5
6
注意看我们是如何舍弃了theName,仅在构造函数里使用private name: string参数来创建和初始化name成员。
我们把声明和赋值合并至一处。

参数属性通过给构造函数参数添加一个访问限定符来声明。
使用private限定一个参数属性会声明并初始化一个私有成员;对于public和protected来说也是一样。

存取器
TypeScript支持getters/setters来截取对对象成员的访问。
它能帮助你有效的控制对对象成员的访问。

下面来看如何把一类改写成使用get和set。
首先是一个没用使用存取器的例子。

class Employee {
fullName: string;
}

var employee = new Employee();
employee.fullName = “Bob Smith”;
if (employee.fullName) {
console.log(employee.fullName);
}
1
2
3
4
5
6
7
8
9
我们可以随意的设置fullName,这是非常方便的,但是这也可能会带来麻烦。

下面这个版本里,我们先检查用户密码是否正确,然后再允许其修改employee。
我们把对fullName的直接访问改成了可以检查密码的set方法。
我们也加了一个get方法,让上面的例子仍然可以工作。

var passcode = “secret passcode”;

class Employee {
private _fullName: string;

get fullName(): string {
    return this._fullName;
}

set fullName(newName: string) {
    if (passcode && passcode == "secret passcode") {
        this._fullName = newName;
    }
    else {
        console.log("Error: Unauthorized update of employee!");
    }
}

}

var employee = new Employee();
employee.fullName = “Bob Smith”;
if (employee.fullName) {
alert(employee.fullName);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
我们可以修改一下密码,来验证一下存取器是否是工作的。当密码不对时,会提示我们没有权限去修改employee。

注意:若要使用存取器,要求设置编译器输出目标为ECMAScript 5或更高。

静态属性
到目前为止,我们只讨论了类的实例成员,那些仅当类被实例化的时候才会被初始化的属性。
我们也可以创建类的静态成员,这些属性存在于类本身上面而不是类的实例上。
在这个例子里,我们使用static定义origin,因为它是所有网格都会用到的属性。
每个实例想要访问这个属性的时候,都要在origin前面加上类名。
如同在实例属性上使用this.前缀来访问属性一样,这里我们使用Grid.来访问静态属性。

class Grid {
static origin = {x: 0, y: 0};
calculateDistanceFromOrigin(point: {x: number; y: number;}) {
var xDist = (point.x - Grid.origin.x);
var yDist = (point.y - Grid.origin.y);
return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
}
constructor (public scale: number) { }
}

var grid1 = new Grid(1.0); // 1x scale
var grid2 = new Grid(5.0); // 5x scale

console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
高级技巧
构造函数
当你在TypeScript里定义类的时候,实际上同时定义了很多东西。
首先是类的实例的类型。

class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}

var greeter: Greeter;
greeter = new Greeter(“world”);
console.log(greeter.greet());
1
2
3
4
5
6
7
8
9
10
11
12
13
在这里,我们写了var greeter: Greeter,意思是Greeter类实例的类型是Greeter。
这对于用过其它面向对象语言的程序员来讲已经是老习惯了。

我们也创建了一个叫做构造函数的值。
这个函数会在我们使用new创建类实例的时候被调用。
下面我们来看看,上面的代码被编译成JavaScript后是什么样子的:

var Greeter = (function () {
function Greeter(message) {
this.greeting = message;
}
Greeter.prototype.greet = function () {
return "Hello, " + this.greeting;
};
return Greeter;
})();

var greeter;
greeter = new Greeter(“world”);
console.log(greeter.greet());
1
2
3
4
5
6
7
8
9
10
11
12
13
上面的代码里,var Greeter将被赋值为构造函数。
当我们使用new并执行这个函数后,便会得到一个类的实例。
这个构造函数也包含了类的所有静态属性。
换个角度说,我们可以认为类具有实例部分与静态部分这两个部分。

让我们来改写一下这个例子,看看它们之前的区别:

class Greeter {
static standardGreeting = “Hello, there”;
greeting: string;
greet() {
if (this.greeting) {
return "Hello, " + this.greeting;
}
else {
return Greeter.standardGreeting;
}
}
}

var greeter1: Greeter;
greeter1 = new Greeter();
console.log(greeter1.greet());

var greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = “Hey there!”;
var greeter2:Greeter = new greeterMaker();
console.log(greeter2.greet());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
这个例子里,greeter1与之前看到的一样。
我们实例化Greeter类,并使用这个对象。
与我们之前看到的一样。

再之后,我们直接使用类。
我们创建了一个叫做greeterMaker的变量。
这个变量保存了这个类或者说保存了类构造函数。
然后我们使用typeof Greeter,意思是取Greeter类的类型,而不是实例的类型。
或者理确切的说,”告诉我Greeter标识符的类型”,也就是构造函数的类型。
这个类型包含了类的所有静态成员和构造函数。
之后,就和前面一样,我们在greeterMaker上使用new,创建Greeter的实例。

把类当做接口使用
如上一节里所讲的,类定义会创建两个东西:类实例的类型和一个构造函数。
因为类可以创建出类型,所以你能够在可以使用接口的地方使用类。

class Point {
x: number;
y: number;
}

interface Point3d extends Point {
z: number;
}

var point3d: Point3d = {x: 1, y: 2, z: 3};
1
2
3
4
5
6
7
8
9
10
关于术语的一点说明:
必须要注意一点在TypeScript 1.5里,术语名称已经发生了变化。
“Internal modules” 现在叫做 “namespaces”。
“External modules” 现在则简称为 “modules”,为了与ECMAScript 2015里的术语保持一致,(也就是说 module X { 相当于现在推荐的写法 namespace X {)。

命名空间/模块 介绍
这篇文章将概括介绍在TypeScript里使用模块与命名空间组织代码的方法。
我们也会谈及命名空间和模块的高级使用场景,并指出在使用它们的过程中常见的陷井。

查看模块章节了解关于模块的更多信息。
查看命名空间章节了解关于命名空间的更多信息。

使用命名空间
命名空间是在全局命名空间里的一个有名字的JavaScript普通对象。
这令命名空间十分容易使用。
它们可以在多文件中同时使用,并通过–outFile结合在一起。
命名空间是帮助你组织Web应用的好助手,可以把所有依赖都放在页面的

但就像其它全局命名空间污染一样,这很难去了解组件之间的依赖关系,尤其是在大型的应用中。

拥抱模块化
像命名空间一样,模块可以包含代码和声明。
不同的是模块可以声明它的依赖。

模块也会把依赖添加到模块加载器上(例如CommonJs/requirejs)。
对于小型的JS应用来说这可能是不必要的,但是对于大型应用,这一点点的花费会带来长久的模块化和可维护性上的便利。
模块也提供了更好的代码重用,更强的封闭性和更好的支持用工具进行优化。

对于Node.js应用来说,模块是默认的并组是推荐的组织代码的方式。

从ECMAScript 2015开始,模块成为了语言内置的部分,应该会被所有正常的解释引擎所支持。

对于新的项目来说模块应该是首选组织代码的形式。

命名空间和模块的陷井
这部分我们会描述常见的命名空间和模块的使用陷井,和怎样去避免它。

对模块使用///
一个常见的错误是使用/// 引用模块文件,应该使用import。
要理解这之间的不同,我们首先应该弄清编译器定位模块类型信息的3种方法。

首先,根据import x = require(…);声明查找.ts文件。
这个文件应该是使用了顶层import或export声明的具体实现文件。

其次,与前一步相似,去查找.d.ts文件,不同的是它不是具体实现文件而是声明文件(同样具有顶级的import或export声明)。

最后,是查找“外部模块的声明”,它是通过declare和使用被引号括住的名字定义的。

myModules.d.ts
// In a .d.ts file or .ts file that is not a module:
declare module “SomeModule” {
export function fn(): string;
}
1
2
3
4
myOtherModule.ts
///
import m = require(“SomeModule”);
1
2
这里的引用标签指定了外来模块的位置。
这就是一些Typescript例子中引用node.d.ts的方法。

不必要的命名空间
如果你想把命名空间转换为模块,它可能会像下面这个文件一件:

shapes.ts
export namespace Shapes {
export class Triangle { /* … / }
export class Square { /
… */ }
}
1
2
3
4
顶层的模块Shapes包裹了Triangle和Square。
这对于使用它的人来说是让人迷惑和讨厌的:

shapeConsumer.ts
import shapes = require(’./shapes’);
var t = new shapes.Shapes.Triangle(); // shapes.Shapes?
1
2
TypeScript里模块的一个特点是不同的模块永远也不会在相同的作用域内使用相同的名字。
因为使用模块的人会为它们命名,所以完全没有必要把导出的符号包裹在一个命名空间里。

再次重申,不应该对模块使用命名空间,使用命名空间是为了提供逻辑分组和避免命名冲突。
模块文件本身已经是一个逻辑分组,并且它的名字是由导入这个模块的代码指定,所以没有必要为导出的对象增加额外的模块层。

下面是改进的例子:

shapes.ts
export class Triangle { /* … / }
export class Square { /
… */ }
1
2
shapeConsumer.ts
import shapes = require(’./shapes’);
var t = new shapes.Triangle();
1
2
模块的取舍
就像每个JS文件对应一个模块一样,TypeScript里模块文件与生成的JS文件也是一一对应的。
这会产生一个效果,就是无法使用–out来让编译器合并多个模块文件为一个JavaScript文件。

关于术语的一点说明:
必须要注意一点在TypeScript 1.5里,术语名称已经发生了变化。
“Internal modules” 现在叫做 “namespaces”。
“External modules” 现在则简称为 “modules”,为了与ECMAScript 2015里的术语保持一致,(也就是说 module X { 相当于现在推荐的写法 namespace X {)。

命名空间 介绍
这篇文章描述了如何在TypeScript里使用命名空间(之前叫做“内部模块”)来组织你的代码。

就像我们在术语说明里提到的那样,“内部模块”现在叫做“命名空间”。

另外,任何使用module关键字来声明一个内部模块的地方都应该使用namespace关键字来替换。

这就避免了让新的使用者被相似的名称所迷惑。

第一步
我们先来写一段程序并将在整篇文章中都使用这个例子。
我们定义几个简单的字符串验证器,假设你会使用它们来验证表单里的用户输入或验证外部数据。

所有的验证器都放在一个文件里
interface StringValidator {
isAcceptable(s: string): boolean;
}

var lettersRegexp = /1+ / ; v a r n u m b e r R e g e x p = / [ 0 − 9 ] + /; var numberRegexp = /^[0-9]+ /;varnumberRegexp=/[09]+/;

class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}

class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}

// Some samples to try
var strings = [“Hello”, “98052”, “101”];
// Validators to use
var validators: { [s: string]: StringValidator; } = {};
validators[“ZIP code”] = new ZipCodeValidator();
validators[“Letters only”] = new LettersOnlyValidator();
// Show whether each string passed each validator
strings.forEach(s => {
for (var name in validators) {
console.log(""" + s + “” " + (validators[name].isAcceptable(s) ? " matches " : " does not match ") + name);
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
命名空间
随着更多验证器的加入,我们需要一种手段来组织代码,以便于在记录它们的类型的同时还不用担心与其它对象产生命名冲突。
因此,我们把验证器包裹到一个命名空间内,而不是把它们放在全局命名空间下。

下面的例子里,把所有与验证器相关的类型都放到一个叫做Validation的命名空间里。
因为我们想让这些接口和类在命名空间之外也是可访问的,所以需要使用export。
相反的,变量lettersRegexp和numberRegexp是实现的细节,不需要导出,因此它们在命名空间外是不能访问的。
在文件末尾的测试代码里,由于是在命名空间之外访问,因此需要限定类型的名称,比如Validation.LettersOnlyValidator。

使用命名空间的验证器
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}

const lettersRegexp = /^[A-Za-z]+$/;
const numberRegexp = /^[0-9]+$/;

export class LettersOnlyValidator implements StringValidator {
    isAcceptable(s: string) {
        return lettersRegexp.test(s);
    }
}

export class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}

}

// Some samples to try
var strings = [“Hello”, “98052”, “101”];
// Validators to use
var validators: { [s: string]: Validation.StringValidator; } = {};
validators[“ZIP code”] = new Validation.ZipCodeValidator();
validators[“Letters only”] = new Validation.LettersOnlyValidator();
// Show whether each string passed each validator
strings.forEach(s => {
for (var name in validators) {
console.log("${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name });
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
分离到多文件
当应用变得越来越大时,我们需要将代码分离到不同的文件中以便于维护。

多文件中的命名空间
现在,我们把Validation命名空间分割成多个文件。
尽管是不同的文件,它们仍是同一个命名空间,并且在使用的时候就如同它们在一个文件中定义的一样。
因为不同文件之间存在依赖关系,所以我们加入了引用标签来告诉编译器文件之间的关联。
我们的测试代码保持不变。

Validation.ts
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}
}
1
2
3
4
5
LettersOnlyValidator.ts
///
namespace Validation {
const lettersRegexp = /2+KaTeX parse error: Expected 'EOF', got '}' at position 163: … } } }̲ 1 2 3 4 5 6 7 …/;
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
}
1
2
3
4
5
6
7
8
9
Test.ts
///
///
///

// Some samples to try
var strings = [“Hello”, “98052”, “101”];
// Validators to use
var validators: { [s: string]: Validation.StringValidator; } = {};
validators[“ZIP code”] = new Validation.ZipCodeValidator();
validators[“Letters only”] = new Validation.LettersOnlyValidator();
// Show whether each string passed each validator
strings.forEach(s => {
for (var name in validators) {
console.log(""" + s + “” " + (validators[name].isAcceptable(s) ? " matches " : " does not match ") + name);
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
当涉及到多文件时,我们必须确保所有编译后的代码都被加载了。
我们有两种方式。

第一种方式,把所有的输入文件编译为一个输出文件,需要使用–out标记:

tsc --outFile sample.js Test.ts
1
编译器会根据源码里的引用标签自动地对输出进行排序。你也可以单独地指定每个文件。

tsc --outFile sample.js Validation.ts LettersOnlyValidator.ts ZipCodeValidator.ts Test.ts
1
第二种方式,我们可以编译每一个文件(默认方式),那么每个源文件都会对应生成一个JavaScript文件。
然后,在页面上通过

MyTestPage.html (excerpt)

namespace Shapes {
export namespace Polygons {
export class Triangle { }
export class Square { }
}
}

import polygons = Shapes.Polygons;
var sq = new polygons.Square(); // Same as “new Shapes.Polygons.Square()”
1
2
3
4
5
6
7
8
9
注意,我们并没有使用require关键字,而是直接使用导入符号的限定名赋值。
这与使用var相似,但它还适用于类型和导入的具有命名空间含义的符号。
重要的是,对于值来讲,import会生成与原始符号不同的引用,所以改变别名的值并不会影响原始变量的值。

使用其它的JavaScript库
为了描述不是用TypeScript编写的类库的类型,我们需要声明类库导出的API。
由于大部分程序库只提供少数的顶级对象,命名空间是用来表示它们是一个好办法。

我们叫它声明因为它不是外部程序的具体实现。
通常会在.d.ts里写这些定义。
如果你熟悉C/C++,你可以把它们当做.h文件。
让我们看一些例子。

外部命名空间
流行的程序库D3在全局对象d3里定义它的功能。
因为这个库通过一个

D3.d.ts (部分摘录)
declare namespace D3 {
export interface Selectors {
select: {
(selector: string): Selection;
(element: EventTarget): Selection;
};
}

export interface Event {
    x: number;
    y: number;
}

export interface Base extends Selectors {
    event: Event;
}

}

declare var d3: D3.Base;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
关于术语的一点说明:
必须要注意一点在TypeScript 1.5里,术语名称已经发生了变化。
“Internal modules” 现在叫做 “namespaces”。
“External modules” 现在则简称为 “modules”,为了与ECMAScript 2015里的术语保持一致,(也就是说 module X { 相当于现在推荐的写法 namespace X {)。

模块 介绍
从ECMAScript 2015开始,JavaScript引入了模块的概念。TypeScript也沿用这个概念。

模块在其自身的作用域里执行,而不是在全局作用域里;这意味着定义在一个模块里的变量,函数,类等等在模块外部是不可见的,除非你明确地使用export forms语法其中的一个导出它们。
相反,如果想使用其它模块导出的变量,函数,类,接口等的时候,你必须要导入它们,可以使用import 形式之一。

模块是自声明的;两个模块之间的关系是通过在文件级别上使用imports和exports建立的。

模块使用模块加载器去导入其它的模块。
在运行时,模块加载器的作用在执行此模块代码前去查找并执行这个模块的所有依赖。
大家最熟知的JavaScript模块加载器是服务于Node.js的CommonJS和服务于Web应用的require.js。

TypeScript与CMAScript 2015一样,任何包含顶级import或者export的文件都被当成一个模块。

Export 导出
导出声明
任何声明(比如变量,函数,类,类型别名或接口)都能够通过添加export关键字来导出。

Validation.ts
export interface StringValidator {
isAcceptable(s: string): boolean;
}
1
2
3
ZipCodeValidator.ts
export const numberRegexp = /3+$/;

export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
1
2
3
4
5
6
7
导出语句
导出语句很便利,因为我们可能需要对导出的部分重命名,所以上面的例子可以这样改写:

class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
export { ZipCodeValidator };
export { ZipCodeValidator as mainValidator };
1
2
3
4
5
6
7
重新导出
我们经常会去扩展其它模块,并且只导出那个模块的部分内容。
重新导出功能并不会在当前模块导入那个模块或定义一个新的局部变量。

ParseIntBasedZipCodeValidator.ts
export class ParseIntBasedZipCodeValidator {
isAcceptable(s: string) {
return s.length === 5 && parseInt(s).toString() === s;
}
}

// 导出原先的验证器但做了重命名
export {ZipCodeValidator as RegExpBasedZipCodeValidator} from “./ZipCodeValidator”;
1
2
3
4
5
6
7
8
或者一个模块可以包裹多个模块,并把他们导出的内容联合在一起通过语法:export * from “module”。

AllValidators.ts
export * from “./StringValidator”; // exports interface StringValidator
export * from “./LettersOnlyValidator”; // exports class LettersOnlyValidator
export * from “./ZipCodeValidator”; // exports class ZipCodeValidator
1
2
3
Import 导入
模块的导入操作与导出一样简单。
可以使用以下import形式之一来导入其它模块中的导出内容。

导入一个模块中的某一个export内容
import { ZipCodeValidator } from “./ZipCodeValidator”;

var myValidator = new ZipCodeValidator();
1
2
3
可以对导入内容重命名

import { ZipCodeValidator as ZCV } from “./ZipCodeValidator”;
var myValidator = new ZCV();
1
2
将整个模块导入到一个变量,并通过它来访问模块的导出部分
import * as validator from “./ZipCodeValidator”;
var myValidator = new validator.ZipCodeValidator();
1
2
具有副作用的导入模块
尽管不推荐这么做,一些模块会设置一些全局状态供其它模块使用。
这些模块可能没有任何的exports或用户根本就不关注它的exports。
使用下面的方法来导入这类模块:

import “./my-module.js”;
1
Default exports
每个模块都可以有一个default导出。
默认导出使用default关键字标记;并且一个模块只能够有一个default导出。
引入default导出的时候,需要使用另外一种导出形式。

default导出十分便利。
比如,像JQuery这样的类库可能有一个默认导出jQuery或 , 并 且 我 们 基 本 上 也 会 使 用 同 样 的 名 字 j Q u e r y 或 ,并且我们基本上也会使用同样的名字jQuery或 使jQuery导出JQuery。

JQuery.d.ts
declare var $: JQuery;
export default $;
1
2
App.ts
import $ from “JQuery”;

$(“button.continue”).html( “Next Step…” );
1
2
3
类和函数声明可以直接被标记为默认导出。
标记为默认导出的类和函数的名字是可以省略的。

ZipCodeValidator.ts
export default class ZipCodeValidator {
static numberRegexp = /4+$/;
isAcceptable(s: string) {
return s.length === 5 && ZipCodeValidator.numberRegexp.test(s);
}
}
1
2
3
4
5
6
Test.ts
import validator from “./ZipCodeValidator”;

var validator = new validator();
1
2
3
或者

StaticZipCodeValidator.ts
const numberRegexp = /5+$/;

export default function (s: string) {
return s.length === 5 && numberRegexp.test(s);
}
1
2
3
4
5
Test.ts
import validate from “./StaticZipCodeValidator”;

var strings = [“Hello”, “98052”, “101”];

// Use function validate
strings.forEach(s => {
console.log("${s}" ${validate(s) ? " matches" : " does not match"});
});
1
2
3
4
5
6
7
8
default导出也可以是一个值

OneTwoThree.ts
export default “123”;
1
Log.ts
import num from “./OneTwoThree”;

console.log(num); // “123”
1
2
3
export = 和 import = require()
CommonJS和AMD都有一个exports对象的概念,它包含了一个模块的所有导出内容。

它们也支持把exports替换为一个自定义对象。
默认导出就好比这样一个功能;然而,它们却并不相互兼容。
TypeScript模块支持export =语法,以配合传统的CommonJS和AMD的工作流。

export =语法定义一个模块的导出对象。
它可以是类,接口,命名空间,函数或枚举。

若要导入一个使用了export =的模块时,必须使用TypeScript提供的特定语法import var = require(“module”)。

ZipCodeValidator.ts
var numberRegexp = /6+$/;
class ZipCodeValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
export = ZipCodeValidator;
1
2
3
4
5
6
7
Test.ts
import zip = require("./ZipCodeValidator");

// Some samples to try
var strings = [“Hello”, “98052”, “101”];

// Validators to use
var validator = new zip.ZipCodeValidator();

// Show whether each string passed each validator
strings.forEach(s => {
console.log("${ s }" - ${ validator.isAcceptable(s) ? "matches" : "does not match" });
});
1
2
3
4
5
6
7
8
9
10
11
12
生成模块代码
根据编译时指定的模块目标参数,编译器会生成相应的供Node.js (CommonJS),require.js (AMD),isomorphic (UMD), SystemJS或ECMAScript 2015 native modules (ES6)模块加载系统使用的代码。
想要了解生成代码中define,require 和 register的意义,请参考相应模块加载器的文档。

下面的例子说明了导入导出语句里使用的名字是怎么转换为相应的模块加载器代码的。

SimpleModule.ts
import m = require(“mod”);
export var t = m.something + 1;
1
2
AMD / RequireJS SimpleModule.js
define([“require”, “exports”, “./mod”], function (require, exports, mod_1) {
exports.t = mod_1.something + 1;
});
1
2
3
CommonJS / Node SimpleModule.js
var mod_1 = require("./mod");
exports.t = mod_1.something + 1;
1
2
UMD SimpleModule.js
(function (factory) {
if (typeof module === “object” && typeof module.exports === “object”) {
var v = factory(require, exports); if (v !== undefined) module.exports = v;
}
else if (typeof define === “function” && define.amd) {
define([“require”, “exports”, “./mod”], factory);
}
})(function (require, exports) {
var mod_1 = require("./mod");
exports.t = mod_1.something + 1;
});
1
2
3
4
5
6
7
8
9
10
11
System SimpleModule.js
System.register(["./mod"], function(exports_1) {
var mod_1;
var t;
return {
setters:[
function (mod_1_1) {
mod_1 = mod_1_1;
}],
execute: function() {
exports_1(“t”, t = mod_1.something + 1);
}
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
Native ECMAScript 2015 modules SimpleModule.js
import { something } from “./mod”;
export var t = something + 1;
1
2
简单示例
下面我们来整理一下前面的验证器实现,每个模块只有一个命名的导出。

为了编译,我们必需要在命令行上指定一个模块目标。对于Node.js来说,使用–module commonjs;
对于require.js来说,使用`–module amd。比如:

tsc --module commonjs Test.ts
1
When compiled, each module will become a separate .js file.
As with reference tags, the compiler will follow import statements to compile dependent files.
编译完成后,每个模块会生成一个单独的.js文件。
好比使用了reference标签,编译器会根据import语句编译相应的文件。

Validation.ts
export interface StringValidator {
isAcceptable(s: string): boolean;
}
1
2
3
LettersOnlyValidator.ts
import { StringValidator } from “./Validation”;

const lettersRegexp = /7+$/;

export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}
1
2
3
4
5
6
7
8
9
ZipCodeValidator.ts
import { StringValidator } from “./Validation”;

const numberRegexp = /8+$/;

export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
1
2
3
4
5
6
7
8
9
Test.ts
import { StringValidator } from “./Validation”;
import { ZipCodeValidator } from “./ZipCodeValidator”;
import { LettersOnlyValidator } from “./LettersOnlyValidator”;

// Some samples to try
let strings = [“Hello”, “98052”, “101”];

// Validators to use
let validators: { [s: string]: StringValidator; } = {};
validators[“ZIP code”] = new ZipCodeValidator();
validators[“Letters only”] = new LettersOnlyValidator();

// Show whether each string passed each validator
strings.forEach(s => {
for (var name in validators) {
console.log("${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name });
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
可选的模块加载和其它高级加载场景
有时候,你只想在某种条件下才加载某个模块。
在TypeScript里,则使用下面的方式来实现它和其它的高级加载场景,我们可以直接调用模块加载器并且可以保证类型完全。

编译器会探测是否每个模块都会在生成的JavaScript中用到。
如果一个模块标识符只被当成是类型注解部分使用时并完全没有在表达式中使用时,就不会生成require这个模块的代码。
省略掉没有用到的引用对性能提升是很有益的,并同时提供了可选加载模块的能力。

这种模式的核心是import id = require("…")语句可以让我们访问模块导出的类型。
模块加载器会被动态调用(通过require),就像下面if代码块里那样。
它利用了省略引用的优化,所以模块只在被需要时加载。
为了让这个模块工作,一定要注意import定义的标识符只能在表示类型处使用(不能在会转换成JavaScript的地方)。

为了确保类型安全性,我们可以使用typeof关键字。
typeof关键字,当在表示类型的地方使用时,会得出一个类型值,这里就表示模块的类型。

示例:Node.js里的动态模块加载
declare var require;

import { ZipCodeValidator as Zip } from “./ZipCodeValidator”;

if (needZipValidation) {
var x: typeof Zip = require("./ZipCodeValidator");
if (x.isAcceptable("…")) { /* … */ }
}
1
2
3
4
5
6
7
8
示例:require.js里的动态模块加载
declare var require;

import { ZipCodeValidator as Zip } from “./ZipCodeValidator”;

if (needZipValidation) {
require(["./ZipCodeValidator"], (x: typeof Zip) => {
if (x.isAcceptable("…")) { /* … */ }
});
}
1
2
3
4
5
6
7
8
9
示例:System.js里的动态模块加载
declare var System;

import { ZipCodeValidator as Zip } from “./ZipCodeValidator”;

if (needZipValidation) {
System.import("./ZipCodeValidator").then((x: typeof Zip) => {
if (x.isAcceptable("…")) { /* … */ }
});
}
1
2
3
4
5
6
7
8
9
使用其它的JavaScript库
为了描述不是用TypeScript编写的类库的类型,我们需要声明类库导出的API。

我们叫它声明因为它不是外部程序的具体实现。
通常会在.d.ts里写这些定义。
如果你熟悉C/C++,你可以把它们当做.h文件。
让我们看一些例子。

外部模块
在Node.js里大部分工作是通过加载一个或多个模块实现的。
我们可以使用顶级的export声明来为每个模块都定义一个.d.ts文件,但最好还是写在一个大的.d.ts文件里。
我们使用与构造一个外部命名空间相似的方法,但是这里使用module关键字并且把名字用引号括起来,方便之后import。
例如:

node.d.ts (simplified excerpt)
declare module “url” {
export interface Url {
protocol?: string;
hostname?: string;
pathname?: string;
}

export function parse(urlStr: string, parseQueryString?, slashesDenoteHost?): Url;

}

declare module “path” {
export function normalize(p: string): string;
export function join(…paths: any[]): string;
export var sep: string;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
现在我们可以/// node.d.ts并且使用import url = require(“url”);加载模块。

///
import * as URL from “url”;
var myUrl = URL.parse(“http://www.typescriptlang.org”);
1
2
3
创建模块结构指导
尽可能地在顶层导出
用户应该更容易地使用你模块导出的内容。
嵌套层次过多会变得难以处理,因此仔细考虑一下如何组织你的代码。

从你的模块中导出一个命名空间就是一个增加嵌套的例子。
虽然命名空间有时候有它们的用处,在使用模块的时候它们额外地增加了一层。
这对用户来说是很不便的并且通常是多余的。

Static methods on an exported class have a similar problem - the class itself adds a layer of nesting.
Unless it increases expressivity or intent in a clearly useful way, consider simply exporting a helper function.
导出类的静态方法也有同样的问题 - 这个类本身就增加了一层嵌套。
除非它能方便表述或便于清晰使用,否则请考虑直接导出一个辅助方法。

如果仅导出单个 class 或 function,使用 export default
就像“在顶层上导出”帮助减少用户使用的难度,一个默认的导出也能起到这个效果。
如果一个模块就是为了导出特定的内容,那么你应该考虑使用一个默认导出。
这会令模块的导入和使用变得些许简单。
比如:

MyClass.ts
export default class SomeType {
constructor() { … }
}
1
2
3
MyFunc.ts
export default function getThing() { return ‘thing’; }
1
Consumer.ts
import t from “./MyClass”;
import f from “./MyFunc”;
var x = new t();
console.log(f());
1
2
3
4
对用户来说这是最理想的。他们可以随意命名导入模块的类型(本例为t)并且不需要多余的(.)来找到相关对象。

如果要导出多个对象,把它们放在顶层里导出
MyThings.ts
export class SomeType { /* … / }
export function someFunc() { /
… */ }
1
2
相反地,当导入的时候:

明确地列出导入的名字
Consumer.ts
import { SomeType, SomeFunc } from “./MyThings”;
var x = new SomeType();
var y = someFunc();
1
2
3
使用命名空间导入模式当你要导出大量内容的时候
MyLargeModule.ts
export class Dog { … }
export class Cat { … }
export class Tree { … }
export class Flower { … }
1
2
3
4
Consumer.ts
import * as myLargeModule from “./MyLargeModule.ts”;
var x = new myLargeModule.Dog();
1
2
使用重新导出进行扩展
你可能经常需要去扩展一个模块的功能。
JS里常用的一个模式是JQuery那样去扩展原对象。
如我们之前提到的,模块不会像全局命名空间对象那样去合并。
推荐的方案是不要去改变原来的对象,而是导出一个新的实体来提供并提的功能。

假设Calculator.ts模块里定义了一个简单的计算器实现。
这个模块同样提供了一个辅助函数来测试计算器的功能,通过传入一系列输入的字符串并在最后给出结果。

Calculator.ts
export class Calculator {
private current = 0;
private memory = 0;
private operator: string;

protected processDigit(digit: string, currentValue: number) {
    if (digit >= "0" && digit <= "9") {
        return currentValue * 10 + (digit.charCodeAt(0) - "0".charCodeAt(0));
    }
}

protected processOperator(operator: string) {
    if (["+", "-", "*", "/"].indexOf(operator) >= 0) {
        return operator;
    }
}

protected evaluateOperator(operator: string, left: number, right: number): number {
    switch (this.operator) {
        case "+": return left + right;
        case "-": return left - right;
        case "*": return left * right;
        case "/": return left / right;
    }
}

private evaluate() {
    if (this.operator) {
        this.memory = this.evaluateOperator(this.operator, this.memory, this.current);
    }
    else {
        this.memory = this.current;
    }
    this.current = 0;
}

public handelChar(char: string) {
    if (char === "=") {
        this.evaluate();
        return;
    }
    else {
        let value = this.processDigit(char, this.current);
        if (value !== undefined) {
            this.current = value;
            return;
        }
        else {
            let value = this.processOperator(char);
            if (value !== undefined) {
                this.evaluate();
                this.operator = value;
                return;
            }
        }
    }
    throw new Error(`Unsupported input: '${char}'`);
}

public getResult() {
    return this.memory;
}

}

export function test(c: Calculator, input: string) {
for (let i = 0; i < input.length; i++) {
c.handelChar(input[i]);
}

console.log(`result of '${input}' is '${c.getResult()}'`);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
这是使用导出的test函数来测试计算器。

TestCalculator.ts
import { Calculator, test } from “./Calculator”;

var c = new Calculator();
test(c, “1+2*33/11=”); // prints 9
1
2
3
4
5
现在扩展它,添加支持输入其它进制(十进制以外),让我们来创建ProgrammerCalculator.ts。

ProgrammerCalculator.ts

import { Calculator } from “./Calculator”;

class ProgrammerCalculator extends Calculator {
static digits = [“0”, “1”, “2”, “3”, “4”, “5”, “6”, “7”, “8”, “9”, “A”, “B”, “C”, “D”, “E”, “F”];

constructor(public base: number) {
    super();
    if (base <= 0 || base > ProgrammerCalculator.digits.length) {
        throw new Error("base has to be within 0 to 16 inclusive.");
    }
}

protected processDigit(digit: string, currentValue: number) {
    if (ProgrammerCalculator.digits.indexOf(digit) >= 0) {
        return currentValue * this.base + ProgrammerCalculator.digits.indexOf(digit);
    }
}

}

// Export the new extended calculator as Calculator
export { ProgrammerCalculator as Calculator };

// Also, export the helper function
export { test } from “./Calculator”;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
新的ProgrammerCalculator模块导出的API与原先的Calculator模块很相似,但却没有改变原模块里的对象。
下面是测试ProgrammerCalculator类的代码:

TestProgrammerCalculator.ts
import { Calculator, test } from “./ProgrammerCalculator”;

var c = new Calculator(2);
test(c, “001+010=”); // prints 3
1
2
3
4
5
模块里不要使用命名空间
当初次进入基于模块的开发模式时,可能总会控制不住要将导出包裹在一个命名空间里。
模块具有其自己的作用域,并且只有导出的声明才会在模块外部可见。
记住这点,命名空间在使用模块时几乎没什么价值。

在组织方面,命名空间对于在全局作用域内对逻辑上相关的对象和类型进行分组是很便利的。
例如,在C#里,你会从System.Collections里找到所有集合的类型。
通过将类型有层次地组织在命名空间里,可以方便用户找到与使用那些类型。
然而,模块本身已经存在于文件系统之中,必要地。
我们必须通过路径和文件名找到它们,这已经提供了一种逻辑上的组织形式。
我们可以创建/collections/generic/文件夹,把相应模块放在这里面。

命名空间对解决全局作用域里命名冲突来说是很重要的。
比如,你可以有一个My.Application.Customer.AddForm和My.Application.Order.AddForm – 两个类型的名字相同,但命名空间不同。
然而,这对于模块来说却不是一个问题。
在一个模块里,没有理由两个对象拥有同一个名字。
从模块的使用角度来说,使用者会挑出他们用来引用模块的名字,所以也没有理由发生重名的情况。

更多关于模块和命名空间的资料查看[命名空间和模块](./Namespaces and Modules.md)

危险信号
以下均为模块结构上的危险信号。重新检查以确保你没有在对模块使用命名空间:

A file whose only top-level declaration is export namespace Foo { … } (remove Foo and move everything ‘up’ a level)
A file that has a single export class or export function (consider using export default)
Multiple files that have the same export namespace Foo { at top-level (don’t think that these are going to combine into one Foo!)
文件的顶层声明是export namespace Foo { … } (删除Foo并把所有内容向上层移动一层)
文件只有一个export class或export function (考虑使用export default)
多个文件的顶层具有同样的export namespace Foo { (不要以为这些会合并到一个Foo中!)

函数 介绍
函数是JavaScript应用程序的基础。
它帮助你实现抽象层,模拟类,信息隐藏和模块。
在TypeScript里,虽然已经支持类,命名空间和模块,但函数仍然是主要的定义行为的地方。
TypeScript为JavaScript函数添加了额外的功能,让我们可以更容易的使用。

函数
和JavaScript一样,TypeScript函数可以创建有名字的函数和匿名函数。
你可以随意选择适合应用程序的方式,不论是定义一系列API函数还是只使用一次的函数。

通过下面的例子可以迅速回想起这两种JavaScript中的函数:

// Named function
function add(x, y) {
return x+y;
}

// Anonymous function
var myAdd = function(x, y) { return x+y; };
1
2
3
4
5
6
7
在JavaScript里,函数可以可以使用函数体外部的变量。
当函数这么做时,我们说它‘捕获’了这些变量。
至于为什么可以这样做以及其中的利弊超出了本文的范围,但是深刻理解这个机制对学习JavaScript和TypeScript会很有帮助。

var z = 100;

function addToZ(x, y) {
return x+y+z;
}
1
2
3
4
5
函数类型
为函数定义类型
让我们为上面那个函数添加类型:

function add(x: number, y: number): number {
return x+y;
}

var myAdd = function(x: number, y: number): number { return x+y; };
1
2
3
4
5
我们可以给每个参数添加类型之后再为函数本身添加返回值类型。
TypeScript能够根据返回语句自动推断出返回值类型,因此我们通常省略它。

书写完整函数类型
现在我们已经为函数指定了类型,下面让我们写出函数的完整类型。

var myAdd: (x:number, y:number)=>number =
function(x: number, y: number): number { return x+y; };
1
2
函数类型包含两部分:参数类型和返回值类型。
当写出完整函数类型的时候,这两部分都是需要的。
我们以参数列表的形式写出参数类型,为每个参数指定一个名字和类型。
这个名字只是为了增加可读性。
我们也可以这么写:

var myAdd: (baseValue:number, increment:number)=>number =
function(x: number, y: number): number { return x+y; };
1
2
只要参数类型是匹配的,那么就认为它是有效的函数类型,而不在乎参数名是否正确。

第二部分是返回值类型。
对于返回值,我们在函数和返回值类型之前使用(=>)符号,使之清晰明了。
如之前提到的,返回值类型是函数类型的必要部分,如果函数没有返回任何值,你也必须指定返回值类型为void而不能留空。

函数的类型只是由参数类型和返回值组成的。
函数中使用的捕获变量不会体现在类型里。
实际上,这些变量是函数的隐藏状态并不是组成API的一部分。

推断类型
尝试这个例子的时候,你会发现如果你在赋值语句的一边指定了类型但是另一边没有类型的话,TypeScript编译器会自动识别出类型:

// myAdd has the full function type
var myAdd = function(x: number, y: number): number { return x+y; };

// The parameters x and y have the type number
var myAdd: (baseValue:number, increment:number)=>number =
function(x, y) { return x+y; };
1
2
3
4
5
6
这叫做‘按上下文归类’,是类型推论的一种。
它帮助我们更好地为程序指定类型。

可选参数和默认参数
不同于JavaScript,TypeScript里每个函数参数都是必须的。
这并不是指参数一定是个非null值,而是编译器检查用户是否为每个参数都传入了值。
编译器还会假设只有这些参数会被传递进函数。
简短地说,传递给函数的参数数量必须与函数期望的参数数量一致。

function buildName(firstName: string, lastName: string) {
return firstName + " " + lastName;
}

var result1 = buildName(“Bob”); // error, too few parameters
var result2 = buildName(“Bob”, “Adams”, “Sr.”); // error, too many parameters
var result3 = buildName(“Bob”, “Adams”); // ah, just right
1
2
3
4
5
6
7
JavaScript里,每个参数都是可选的,可传可不传。
没传参的时候,它的值就是undefined。
在TypeScript里我们可以在参数名旁使用?实现可选参数的功能。
比如,我们想让last name是可选的:

function buildName(firstName: string, lastName?: string) {
if (lastName)
return firstName + " " + lastName;
else
return firstName;
}

var result1 = buildName(“Bob”); // works correctly now
var result2 = buildName(“Bob”, “Adams”, “Sr.”); // error, too many parameters
var result3 = buildName(“Bob”, “Adams”); // ah, just right
1
2
3
4
5
6
7
8
9
10
可选参数必须在必须跟在必须参数后面。
如果上例我们想让first name是可选的,那么就必须调整它们的位置,把first name放在后面。

TypeScript里,我们还可以为可选参数设置默认值。
仍然修改上例,把last name的默认值设置为"Smith"。

function buildName(firstName: string, lastName = “Smith”) {
return firstName + " " + lastName;
}

var result1 = buildName(“Bob”); // works correctly now, also
var result2 = buildName(“Bob”, “Adams”, “Sr.”); // error, too many parameters
var result3 = buildName(“Bob”, “Adams”); // ah, just right
1
2
3
4
5
6
7
和可选参数一样,带默认值的参数也要放在必须参数后面。

可选参数与默认值参数共享参数类型。

function buildName(firstName: string, lastName?: string) {
1

function buildName(firstName: string, lastName = “Smith”) {
1
共享同样的类型(firstName: string, lastName?: string) => string。
默认参数的默认值消失了,只保留了它是一个可选参数的信息。

剩余参数
必要参数,默认参数和可选参数有个共同点:它们表示某一个参数。
有时,你想同时操作多个参数,或者你并不知道会有多少参数传递进来。
在JavaScript里,你可以使用arguments来访问所有传入的参数。

在TypeScript里,你可以把所有参数收集到一个变量里:

function buildName(firstName: string, …restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}

var employeeName = buildName(“Joseph”, “Samuel”, “Lucas”, “MacKinzie”);
1
2
3
4
5
剩余参数会被当做个数不限的可选参数。
可以一个都没有,同样也可以有任意个。
编译器创建参数数组,名字是你在省略号(…)后面给定的名字,你可以在函数体内使用这个数组。

这个省略号也会在带有剩余参数的函数类型定义上使用到:

function buildName(firstName: string, …restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}

var buildNameFun: (fname: string, …rest: string[]) => string = buildName;
1
2
3
4
5
Lambda表达式和使用this
JavaScript里this的工作机制对JavaScript程序员来说已经是老生常谈了。
的确,学会如何使用它绝对是JavaScript编程中的一件大事。
由于TypeScript是JavaScript的超集,TypeScript程序员也需要弄清this工作机制并且当有bug的时候能够找出错误所在。
this的工作机制可以单独写一本书了,并确已有人这么做了。在这里,我们只介绍一些基础知识。

JavaScript里,this的值在函数被调用的时候才会指定。
这是个既强大又灵活的特点,但是你需要花点时间弄清楚函数调用的上下文是什么。
众所周知这不是一件很简单的事,特别是函数当做回调函数使用的时候。

下面看一个例子:

var deck = {
suits: [“hearts”, “spades”, “clubs”, “diamonds”],
cards: Array(52),
createCardPicker: function() {
return function() {
var pickedCard = Math.floor(Math.random() * 52);
var pickedSuit = Math.floor(pickedCard / 13);

      return {suit: this.suits[pickedSuit], card: pickedCard % 13};
  }

}
}

var cardPicker = deck.createCardPicker();
var pickedCard = cardPicker();

alert("card: " + pickedCard.card + " of " + pickedCard.suit);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
如果我们运行这个程序,会发现它并没有弹出对话框而是报错了。
因为createCardPicker返回的函数里的this被设置成了window而不是deck对象。
当你调用cardPicker()时会发生这种情况。这里没有对this进行动态绑定因此为window。(注意在严格模式下,会是undefined而不是window)。

为了解决这个问题,我们可以在函数被返回时就绑好正确的this。
这样的话,无论之后怎么使用它,都会引用绑定的‘deck’对象。

我们把函数表达式变为使用lambda表达式( () => {} )。
这样就会在函数创建的时候就指定了‘this’值,而不是在函数调用的时候。

var deck = {
suits: [“hearts”, “spades”, “clubs”, “diamonds”],
cards: Array(52),
createCardPicker: function() {
// Notice: the line below is now a lambda, allowing us to capture this earlier
return () => {
var pickedCard = Math.floor(Math.random() * 52);
var pickedSuit = Math.floor(pickedCard / 13);

      return {suit: this.suits[pickedSuit], card: pickedCard % 13};
  }

}
}

var cardPicker = deck.createCardPicker();
var pickedCard = cardPicker();

alert("card: " + pickedCard.card + " of " + pickedCard.suit);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
为了解更多关于this的信息,请阅读Yahuda Katz的Understanding JavaScript Function Invocation and “this”。

重载
JavaScript本身是个动态语言。
JavaScript里函数根据传入不同的参数而返回不同类型的数据是很常见的。

var suits = [“hearts”, “spades”, “clubs”, “diamonds”];

function pickCard(x): any {
// Check to see if we’re working with an object/array
// if so, they gave us the deck and we’ll pick the card
if (typeof x == “object”) {
var pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
// Otherwise just let them pick the card
else if (typeof x == “number”) {
var pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 };
}
}

var myDeck = [{ suit: “diamonds”, card: 2 }, { suit: “spades”, card: 10 }, { suit: “hearts”, card: 4 }];
var pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);

var pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pickCard方法根据传入参数的不同会返回两种不同的类型。
如果传入的是代表纸牌的对象,函数作用是从中抓一张牌。
如果用户想抓牌,我们告诉他抓到了什么牌。
但是这怎么在类型系统里表示呢。

方法是为同一个函数提供多个函数类型定义来进行函数重载。
编译器会根据这个列表去处理函数的调用。
下面我们来重载pickCard函数。

var suits = [“hearts”, “spades”, “clubs”, “diamonds”];

function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
function pickCard(x): any {
// Check to see if we’re working with an object/array
// if so, they gave us the deck and we’ll pick the card
if (typeof x == “object”) {
var pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
// Otherwise just let them pick the card
else if (typeof x == “number”) {
var pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 };
}
}

var myDeck = [{ suit: “diamonds”, card: 2 }, { suit: “spades”, card: 10 }, { suit: “hearts”, card: 4 }];
var pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);

var pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
这样改变后,重载的pickCard函数在调用的时候会进行正确的类型检查。

为了让编译器能够选择正确的检查类型,它与JavaScript里的处理流程相似。
它查找重载列表,尝试使用第一个重载定义。
如果匹配的话就使用这个。
因此,在定义重载的时候,一定要把最精确的定义放在最前面。

注意,function pickCard(x): any并不是重载列表的一部分,因此这里只有两个重载:一个是接收对象另一个接收数字。
以其它参数调用pickCard会产生错误。

泛型 介绍
软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。
组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。

在像C#和Java这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。
这样用户就可以以自己的数据类型来使用组件。

泛型之Hello World
下面来创建第一个使用泛型的例子:identity函数。
这个函数会返回任何传入它的值。
你可以把这个函数当成是echo命令。

不用泛型的话,这个函数可能是下面这样:

function identity(arg: number): number {
return arg;
}
1
2
3
或者,我们使用any类型来定义函数:

function identity(arg: any): any {
return arg;
}
1
2
3
虽然使用any类型后这个函数已经能接收任何类型的arg参数,但是却丢失了一些信息:传入的类型与返回的类型应该是相同的。
如果我们传入一个数字,我们只知道任何类型的值都有可能被返回。

因此,我们需要一种方法使用返回值的类型与传入参数的类型是相同的。
这里,我们使用了类型变量,它是一种特殊的变量,只用于表示类型而不是值。

function identity(arg: T): T {
return arg;
}
1
2
3
我们给identity添加了类型变量T。
T帮助我们捕获用户传入的类型(比如:number),之后我们就可以使用这个类型。
之后我们再次使用了T当做返回值类型。现在我们可以知道参数类型与返回值类型是相同的了。
这允许我们跟踪函数里使用的类型的信息。

我们把这个版本的identity函数叫做泛型,因为它可以适用于多个类型。
不同于使用any,它不会丢失信息,像第一个例子那像保持准确性,传入数值类型并返回数值类型。

我们定义了泛型函数后,可以用两种方法使用。
第一种是,传入所有的参数,包含类型参数:

var output = identity(“myString”); // type of output will be ‘string’
1
这里我们明确的指定了T是字符串类型,并做为一个参数传给函数,使用了<>括起来而不是()。

第二种方法更普遍。利用了类型推论,编译器会根据传入的参数自动地帮助我们确定T的类型:

var output = identity(“myString”); // type of output will be ‘string’
1
注意我们并没用<>明确的指定类型,编译器看到了myString,把T设置为此类型。
类型推论帮助我们保持代码精简和高可读性。如果编译器不能够自动地推断出类型的话,只能像上面那样明确的传入T的类型,在一些复杂的情况下,这是可能出现的。

使用泛型变量
使用泛型创建像identity这样的泛型函数时,编译器要求你在函数体必须正确的使用这个通用的类型。
换句话说,你必须把这些参数当做是任意或所有类型。

看下之前identity例子:

function identity(arg: T): T {
return arg;
}
1
2
3
如果我们想同时打印出arg的长度。
我们很可能会这样做:

function loggingIdentity(arg: T): T {
console.log(arg.length); // Error: T doesn’t have .length
return arg;
}
1
2
3
4
如果这么做,编译器会报错说我们使用了arg的.length属性,但是没有地方指明arg具有这个属性。
记住,这些类型变量代表的是任意类型,所以使用这个函数的人可能传入的是个数字,而数字是没有.length属性的。

现在假设我们想操作T类型的数组而不直接是T。由于我们操作的是数组,所以.length属性是应该存在的。
我们可以像创建其它数组一样创建这个数组:

function loggingIdentity(arg: T[]): T[] {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
1
2
3
4
你可以这样理解loggingIdentity的类型:泛型函数loggingIdentity,接收类型参数T,和函数arg,它是个元素类型是T的数组,并返回元素类型是T的数组。
如果我们传入数字数组,将返回一个数字数组,因为此时T的的类型为number。
这可以让我们把泛型变量T当做类型的一部分使用,而不是整个类型,增加了灵活性。

我们也可以这样实现上面的例子:

function loggingIdentity(arg: Array): Array {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
1
2
3
4
使用过其它语言的话,你可能对这种语法已经很熟悉了。
在下一节,会介绍如何创建自定义泛型像Array一样。

泛型类型
上一节,我们创建了identity通用函数,可以适用于不同的类型。
在这节,我们研究一下函数本身的类型,以及如何创建泛型接口。

泛型函数的类型与非泛型函数的类型没什么不同,只是有一个类型参数在最前面,像函数声明一样:

function identity(arg: T): T {
return arg;
}

var myIdentity: (arg: T) => T = identity;
1
2
3
4
5
我们也可以使用不同的泛型参数名,只要在数量上和使用方式上能对应上就可以。

function identity(arg: T): T {
return arg;
}

var myIdentity: (arg: U) => U = identity;
1
2
3
4
5
我们还可以使用带有调用签名的对象字面量来定义泛型函数:

function identity(arg: T): T {
return arg;
}

var myIdentity: {(arg: T): T} = identity;
1
2
3
4
5
这引导我们去写第一个泛型接口了。
我们把上面例子里的对象字面量拿出来做为一个接口:

interface GenericIdentityFn {
(arg: T): T;
}

function identity(arg: T): T {
return arg;
}

var myIdentity: GenericIdentityFn = identity;
1
2
3
4
5
6
7
8
9
一个相似的例子,我们可能想把泛型参数当作整个接口的一个参数。
这样我们就能清楚的知道使用的具体是哪个泛型类型(比如:Dictionary而不只是Dictionary)。
这样接口里的其它成员也能知道这个参数的类型了。

interface GenericIdentityFn {
(arg: T): T;
}

function identity(arg: T): T {
return arg;
}

var myIdentity: GenericIdentityFn = identity;
1
2
3
4
5
6
7
8
9
注意,我们的示例做了少许改动。
不再描述泛型函数,而是把非泛型函数签名作为泛型类型一部分。
当我们使用GenericIdentityFn的时候,还得传入一个类型参数来指定泛型类型(这里是:number),锁定了之后代码里使用的类型。
理解何时把参数放在调用签名里和何时放在接口上是很有帮助的,对于描述哪部分类型属于泛型部分来说。

除了泛型接口,我们还可以创建泛型类。
注意,无法创建枚举泛型和命名空间泛型。

泛型类
泛型类看上去与泛型接口差不多。
泛型类使用(<>)括起泛型类型,跟在类名后面。

class GenericNumber {
zeroValue: T;
add: (x: T, y: T) => T;
}

var myGenericNumber = new GenericNumber();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
1
2
3
4
5
6
7
8
GenericNumber类的使用是十分直观的,并且你应该注意到了我们并不限制只能使用数字类型。
也可以使用字符串或其它更复杂的类型。

var stringNumeric = new GenericNumber();
stringNumeric.zeroValue = “”;
stringNumeric.add = function(x, y) { return x + y; };

alert(stringNumeric.add(stringNumeric.zeroValue, “test”));
1
2
3
4
5
与接口一样,直接把泛型类型放在类后面,可以帮助我们确认类的所有属性都在使用相同的类型。

我们在类那节说过,类有两部分:静态部分和实例部分。
泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。

泛型约束
你应该会记得之前的一个例子,我们有时候想操作某类型的一组值,并且我们知道这组值具有什么样的属性。
在loggingIdentity例子中,我们想访问arg的length属性,但是编译器并不能证明每种类型都有length属性,所以就报错了。

function loggingIdentity(arg: T): T {
console.log(arg.length); // Error: T doesn’t have .length
return arg;
}
1
2
3
4
相比于操作any所有类型,我们想要限制函数去处理任意带有.length属性的所有类型。
只要传入的类型有这个属性,我们就允许,就是说至少包含这一属性。
为此,我们需要列出对于T的约束要求。

为此,我们定义一个接口来描述约束条件。
创建一个包含.length属性的接口,使用这个接口和extends关键字还实现约束:

interface Lengthwise {
length: number;
}

function loggingIdentity(arg: T): T {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
1
2
3
4
5
6
7
8
现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:

loggingIdentity(3); // Error, number doesn’t have a .length property
1
我们需要传入符合约束类型的值,必须包含必须的属性:

loggingIdentity({length: 10, value: 3});
1
在泛型约束中使用类型参数
有时候,我们需要使用类型参数去约束另一个类型参数。比如,

function find<T, U extends Findable>(n: T, s: U) { // errors because type parameter used in constraint
// …
}
find (giraffe, myAnimals);
1
2
3
4
可以通过下面的方法来实现,重写上面的代码,

function find(n: T, s: Findable) {
// …
}
find(giraffe, myAnimals);
1
2
3
4
注意: 上面两种写法并不完全等同,因为第一段程序的返回值可能是U,而第二段程序却没有这一限制。

在泛型里使用类类型
在TypeScript使用泛型创建工厂函数时,需要引用构造函数的类类型。比如,

function create(c: {new(): T; }): T {
return new c();
}
1
2
3
一个更高级的例子,使用原型属性推断并约束构造函数与类实例的关系。

class BeeKeeper {
hasMask: boolean;
}

class ZooKeeper {
nametag: string;
}

class Animal {
numLegs: number;
}

class Bee extends Animal {
keeper: BeeKeeper;
}

class Lion extends Animal {
keeper: ZooKeeper;
}

function findKeeper<A extends Animal, K> (a: {new(): A;
prototype: {keeper: K}}): K {

return a.prototype.keeper;
}

findKeeper(Lion).nametag; // typechecks!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
混入 介绍
除了传统的面向对象继承方式,还流行一种通过可重用组件创建类的方式,就是联合另一个简单类的代码。
你可能在Scala等语言里对mixins及其特性已经很熟悉了,但它在JavaScript中也是很流行的。

混入示例
下面的代码演示了如何在TypeScript里使用混入。
后面我们还会解释这段代码是怎么工作的。

// Disposable Mixin
class Disposable {
isDisposed: boolean;
dispose() {
this.isDisposed = true;
}

}

// Activatable Mixin
class Activatable {
isActive: boolean;
activate() {
this.isActive = true;
}
deactivate() {
this.isActive = false;
}
}

class SmartObject implements Disposable, Activatable {
constructor() {
setInterval(() => console.log(this.isActive + " : " + this.isDisposed), 500);
}

interact() {
this.activate();
}

// Disposable
isDisposed: boolean = false;
dispose: () => void;
// Activatable
isActive: boolean = false;
activate: () => void;
deactivate: () => void;
}
applyMixins(SmartObject, [Disposable, Activatable])

var smartObj = new SmartObject();
setTimeout(() => smartObj.interact(), 1000);


// In your runtime library somewhere

function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
derivedCtor.prototype[name] = baseCtor.prototype[name];
})
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
理解这个例子
代码里首先定义了两个类,它们将做为mixins。
可以看到每个类都只定义了一个特定的行为或功能。
稍后我们使用它们来创建一个新类,同时具有这两种功能。

// Disposable Mixin
class Disposable {
isDisposed: boolean;
dispose() {
this.isDisposed = true;
}

}

// Activatable Mixin
class Activatable {
isActive: boolean;
activate() {
this.isActive = true;
}
deactivate() {
this.isActive = false;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
下面创建一个类,结合了这两个mixins。
下面来看一下具体是怎么操作的:

class SmartObject implements Disposable, Activatable {
1
首先应该注意到的是,没使用extends而是使用implements。
把类当成了接口,仅使用Disposable和Activatable的类型而非其实现。
这意味着我们需要在类里面实现接口。
但是这是我们在用mixin时想避免的。

我们可以这么做来达到目的,为将要mixin进来的属性方法创建出占位属性。
这告诉编译器这些成员在运行时是可用的。
这样就能使用mixin带来的便利,虽说需要提前定义一些占位属性。

// Disposable
isDisposed: boolean = false;
dispose: () => void;
// Activatable
isActive: boolean = false;
activate: () => void;
deactivate: () => void;
1
2
3
4
5
6
7
最后,把mixins混入定义的类,完成全部实现部分。

applyMixins(SmartObject, [Disposable, Activatable])
1
最后,创建这个帮助函数,帮我们做混入操作。
它会遍历mixins上的所有属性,并复制到目标上去,把之前的占位属性替换成真正的实现代码。

function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
derivedCtor.prototype[name] = baseCtor.prototype[name];
})
});
}
1
2
3
4
5
6
7
8
声明合并 介绍
TypeScript有一些独特的概念,有的是因为我们需要描述JavaScript顶级对象的类型发生了哪些变化。
这其中之一叫做声明合并。
理解了这个概念,对于你使用TypeScript去操作现有的JavaScript来说是大有帮助的。
同时,也会有助于理解更多高级抽象的概念。

首先,在了解如何进行声明合并之前,让我们先看一下什么叫做声明合并。

在这个手册里,声明合并是指编译器会把两个相同名字的声明合并成一个单独的声明。
合并后的声明同时具有那两个被合并的声明的特性。
声明合并不限于只合并两个,任意数量都可以。

基础概念
Typescript中的声明会创建以下三种实体之一:命名空间,类型或者值。
用于创建命名空间的声明会新建一个命名空间:它包含了可以用(.)符号访问的一些名字。
用于创建类型的声明所做的是:用给定的名字和结构创建一种类型。
最后,创建值的声明就是那些可以在生成的JavaScript里看到的那部分(比如:函数和变量)。

Declaration Type Namespace Type Value
Namespace X X
Class X X
Interface X
Function X
Variable X
理解每个声明创建了什么,有助于理解当声明合并时什么东西被合并了。

理解了每种声明会对应创建什么对于理解如果进行声明合并是有帮助的。

合并接口

最简单最常见的就是合并接口,声明合并的种类是:接口合并。
从根本上说,合并的机制是把各自声明里的成员放进一个同名的单一接口里。

“`TypeScript
interface Box {
height: number;
width: number;
}

interface Box {
scale: number;
}

var box: Box = {height: 5, width: 6, scale: 10};
“`

接口中非函数的成员必须是唯一的。如果多个接口中具有相同名字的非函数成员就会报错。

对于函数成员,每个同名函数声明都会被当成这个函数的一个重载。

需要注意的是,接口A与它后面的接口A(把这个接口叫做A’)合并时,A’中的重载函数具有更高的优先级。

如下例所示:

TypeScript
interface Document {
createElement(tagName: any): Element;
}
interface Document {
createElement(tagName: string): HTMLElement;
}
interface Document {
createElement(tagName: “div”): HTMLDivElement;
createElement(tagName: “span”): HTMLSpanElement;
createElement(tagName: “canvas”): HTMLCanvasElement;
}

这三个接口合并成一个声明。
注意每组接口里的声明顺序保持不变,只是靠后的接口会出现在它前面的接口声明之前。

TypeScript
interface Document {
createElement(tagName: “div”): HTMLDivElement;
createElement(tagName: “span”): HTMLSpanElement;
createElement(tagName: “canvas”): HTMLCanvasElement;
createElement(tagName: string): HTMLElement;
createElement(tagName: any): Element;
}

合并命名空间

与接口相似,同名的命名空间也会合并其成员。
命名空间会创建出命名空间和值,我们需要知道这两者都是怎么合并的。

命名空间的合并,模块导出的同名接口进行合并,构成单一命名空间内含合并后的接口。

值的合并,如果当前已经存在给定名字的命名空间,那么后来的命名空间的导出成员会被加到已经存在的那个模块里。

Animals声明合并示例:

“`TypeScript
namespace Animals {
export class Zebra { }
}

namespace Animals {
export interface Legged { numberOfLegs: number; }
export class Dog { }
}
“`

等同于:

“`TypeScript
namespace Animals {
export interface Legged { numberOfLegs: number; }

export class Zebra { }
export class Dog { }
1
2
}
“`

除了这些合并外,你还需要了解非导出成员是如何处理的。
非导出成员仅在其原始存在于的命名空间(未合并的)之内可见。这就是说合并之后,从其它命名空间合并进来的成员无法访问非导出成员了。

下例提供了更清晰的说明:

“`TypeScript
namespace Animal {
var haveMuscles = true;

export function animalsHaveMuscles() {
return haveMuscles;
}
1
2
3
}

namespace Animal {
export function doAnimalsHaveMuscles() {
return haveMuscles; // <– error, haveMuscles is not visible here
}
}
“`

因为haveMuscles并没有导出,只有animalsHaveMuscles函数共享了原始未合并的命名空间可以访问这个变量。
doAnimalsHaveMuscles函数虽是合并命名空间的一部分,但是访问不了未导出的成员。

命名空间与类和函数和枚举类型合并

命名空间可以与其它类型的声明进行合并。
只要命名空间的定义符合将要合并类型的定义。合并结果包含两者的声明类型。
Typescript使用这个功能去实现一些JavaScript里的设计模式。

首先,尝试将命名空间和类合并。
这让我们可以定义内部类。

TypeScript
class Album {
label: Album.AlbumLabel;
}
namespace Album {
export class AlbumLabel { }
}

合并规则与上面合并命名空间小节里讲的规则一致,我们必须导出AlbumLabel类,好让合并的类能访问。
合并结果是一个类并带有一个内部类。
你也可以使用命名空间为类增加一些静态属性。

除了内部类的模式,你在JavaScript里,创建一个函数稍后扩展它增加一些属性也是很常见的。
Typescript使用声明合并来达到这个目的并保证类型安全。

“`TypeScript
function buildLabel(name: string): string {
return buildLabel.prefix + name + buildLabel.suffix;
}

namespace buildLabel {
export var suffix = “”;
export var prefix = “Hello, “;
}

alert(buildLabel(“Sam Smith”));
“`

相似的,命名空间可以用来扩展枚举型:

“`TypeScript
enum Color {
red = 1,
green = 2,
blue = 4
}

namespace Color {
export function mixColor(colorName: string) {
if (colorName == “yellow”) {
return Color.red + Color.green;
}
else if (colorName == “white”) {
return Color.red + Color.green + Color.blue;
}
else if (colorName == “magenta”) {
return Color.red + Color.blue;
}
else if (colorName == “cyan”) {
return Color.green + Color.blue;
}
}
}
“`

非法的合并

并不是所有的合并都被允许。
现在,类不能与类合并,变量与类型不能合并,接口与类不能合并。
想要模仿类的合并,请参考Mixins in TypeScript。

类型推论 介绍

这节介绍TypeScript里的类型推论。即,类型是在哪里如何被推断的。

基础

TypeScript里,在有些没有明确指出类型的地方,类型推论会帮助提供类型。如下面的例子

TypeScript
var x = 3;

变量x的类型被推断为数字。
这种推断发生在初始化变量和成员,设置默认参数值和决定函数返回值时。

大多数情况下,类型推论是直截了当地。
后面的小节,我们会浏览类型推论时的细微差别。

最佳通用类型

当需要从几个表达式中推断类型时候,会使用这些表达式的类型来推断出一个最合适的通用类型。例如,

TypeScript
var x = [0, 1, null];

为了推断x的类型,我们必须考虑所有元素的类型。
这里有两种选择:number和null。
计算通用类型算法会考虑所有的候选类型,并给出一个兼容所有候选类型的类型。

由于最终的通用类型取自候选类型,有些时候候选类型共享相同的通用类型,但是却没有一个类型能做为所有候选类型的类型。例如:

TypeScript
var zoo = [new Rhino(), new Elephant(), new Snake()];

这里,我们想让zoo被推断为Animal[]类型,但是这个数组里没有对象是Animal类型的,因此不能推断出这个结果。
为了更正,当候选类型不能使用的时候我们需要明确的指出类型:

TypeScript
var zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()];

如果没有找到最佳通用类型的话,类型推论的结果是空对象类型,{}。
因为这个类型没有任何成员,所以访问其成员的时候会报错。

上下文类型

TypeScript类型推论也可能按照相反的方向进行。
这被叫做“按上下文归类”。按上下文归类会发生在表达式的类型与所处的位置相关时。比如:

TypeScript
window.onmousedown = function(mouseEvent) {
console.log(mouseEvent.buton); //<- Error
};

这个例子会得到一个类型错误,TypeScript类型检查器使用Window.onmousedown函数的类型来推断右边函数表达式的类型。
因此,就能推断出mouseEvent参数的类型了。
如果函数表达式不是在上下文类型的位置,mouseEvent参数的类型需要指定为any,这样也不会报错了。

如果上下文类型表达式包含了明确的类型信息,上下文的类型被忽略。
重写上面的例子:

TypeScript
window.onmousedown = function(mouseEvent: any) {
console.log(mouseEvent.buton); //<- Now, no error is given
};

这个函数表达式有明确的参数类型注解,上下文类型被忽略。
这样的话就不报错了,因为这里不会使用到上下文类型。

上下文归类会在很多情况下使用到。
通常包含函数的参数,赋值表达式的右边,类型断言,对象成员和数组字面量和返回值语句。
上下文类型也会做为最佳通用类型的候选类型。比如:

TypeScript
function createZoo(): Animal[] {
return [new Rhino(), new Elephant(), new Snake()];
}

这个例子里,最佳通用类型有4个候选者:Animal,Rhino,Elephant和Snake。
当然,Animal会被做为最佳通用类型。

类型兼容 介绍

TypeScript里的类型兼容性基于结构子类型的。
结构类型是只一种只使用其成员来描述类型的方式。
它正好与名义类型形成对比。
看下面的例子:

“`TypeScript
interface Named {
name: string;
}

class Person {
name: string;
}

var p: Named;
// OK, because of structural typing
p = new Person();
“`

在使用名义类型的语言,比如C#或Java中,这段代码会报错,因为Person类没有明确说明其实现了Named接口。

TypeScript的结构性子类型是根据JavaScript代码的典型写法来设计的。
因为JavaScript里广泛地使用匿名对象,例如函数表达式和对象字面量,所以使用结构类型系统来描述这些类型比使用名义类型系统更好。

关于可靠性的注意事项

TypeScript的类型系统允许一些在编译阶段无法否认其安全性的操作。当一个类型系统具此属性时,被当做是“不可靠”的。TypeScript允许这种不可靠行为的发生是经过仔细考虑的。通过这篇文章,我们会解释什么时候会发生这种情况和其有利的一面。

开始

TypeScript结构化类型系统的基本规则是,如果x要兼容y,那么y至少具有与x相同的属性。比如:

“`TypeScript
interface Named {
name: string;
}

var x: Named;
// y’s inferred type is { name: string; location: string; }
var y = { name: ‘Alice’, location: ‘Seattle’ };
x = y;
“`

这里要检查y是否能赋值给x,编译器检查x中的每个属性,看是否能在y中也找到对应属性。
在这个例子中,y必须包含名字是name的string类型成员。y满足条件,因此赋值正确。

检查函数参数时使用相同的规则:

TypeScript
function greet(n: Named) {
alert('Hello, ’ + n.name);
}
greet(y); // OK

注意,y有个额外的location属性,但这不会引发错误。
只有目标类型(这里是Named)的成员会被一一检查是否兼容。

这个比较过程是递归进行的,检查每个成员及子成员。

比较两个函数

比较原始类型和对象类型时是容易理解的,问题是如何判断两个函数是兼容的。
让我们以两个函数开始,它们仅有参数列表不同:

“`TypeScript
var x = (a: number) => 0;
var y = (b: number, s: string) => 0;

y = x; // OK
x = y; // Error
“`

要查看x是否能赋值给y,首先看它们的参数列表。
x的每个参数必须能在y里找到对应类型的参数。
注意的是参数的名字相同与否无所谓,只看它们的类型。
这里,x的每个参数在y中都能找到对应的参数,所以允许赋值。

第二个赋值错误,因为y有个必需的第二个参数,但是x并没有,所以不允许赋值。

你可能会疑惑为什么允许忽略参数,像例子y = x中那样。
原因是忽略额外的参数在JavaScript里是很常见的。
例如,Array#forEach给回调函数传3个参数:数组元素,索引和整个数组。
尽管如此,传入一个只使用第一个参数的回调函数也是很有用的:

“`TypeScript
var items = [1, 2, 3];

// Don’t force these extra arguments
items.forEach((item, index, array) => console.log(item));

// Should be OK!
items.forEach((item) => console.log(item));
“`

下面来看看如何处理返回值类型,创建两个仅是返回值类型不同的函数:

“`TypeScript
var x = () => ({name: ‘Alice’});
var y = () => ({name: ‘Alice’, location: ‘Seattle’});

x = y; // OK
y = x; // Error because x() lacks a location property
“`

类型系统强制源函数的返回值类型必须是目标函数返回值类型的子类型。

函数参数双向协变

当比较函数参数类型时,只有当源函数参数能够赋值给目标函数或者反过来时才能赋值成功。
这是不稳定的,因为调用者可能传入了一个具有更精确类型信息的函数,但是调用这个传入的函数的时候却使用了不是那么精确的类型信息。
实际上,这极少会发生错误,并且能够实现很多JavaScript里的常见模式。例如:

“`TypeScript
enum EventType { Mouse, Keyboard }

interface Event { timestamp: number; }
interface MouseEvent extends Event { x: number; y: number }
interface KeyEvent extends Event { keyCode: number }

function listenEvent(eventType: EventType, handler: (n: Event) => void) {
/* … */
}

// Unsound, but useful and common
listenEvent(EventType.Mouse, (e: MouseEvent) => console.log(e.x + ‘,’ + e.y));

// Undesirable alternatives in presence of soundness
listenEvent(EventType.Mouse, (e: Event) => console.log((e).x + ‘,’ + (e).y));
listenEvent(EventType.Mouse, <(e: Event) => void>((e: MouseEvent) => console.log(e.x + ‘,’ + e.y)));

// Still disallowed (clear error). Type safety enforced for wholly incompatible types
listenEvent(EventType.Mouse, (e: number) => console.log(e));
“`

可选参数及剩余参数

比较函数兼容性的时候,可选参数与必须参数是可交换的。
原类型上额外的可选参数并不会造成错误,目标类型的可选参数没有对应的参数也不是错误。

当一个函数有剩余参数时,它被当做无限个可选参数。

这对于类型系统来说是不稳定的,但从运行时的角度来看,可选参数一般来说是不强制的,因为对于大多数函数来说相当于传递了一些undefinded。

有一个好的例子,常见的函数接收一个回调函数并用对于程序员来说是可预知的参数但对类型系统来说是不确定的参数来调用:

“`TypeScript
function invokeLater(args: any[], callback: (…args: any[]) => void) {
/* … Invoke callback with ‘args’ … */
}

// Unsound - invokeLater “might” provide any number of arguments
invokeLater([1, 2], (x, y) => console.log(x + ‘, ’ + y));

// Confusing (x and y are actually required) and undiscoverable
invokeLater([1, 2], (x?, y?) => console.log(x + ‘, ’ + y));
“`

函数重载

对于有重载的函数,源函数的每个重载都要在目标函数上找到对应的函数签名。
这确保了目标函数可以在所有源函数可调用的地方调用。
对于特殊的函数重载签名不会用来做兼容性检查。

枚举

枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的。比如,

“`TypeScript
enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };

var status = Status.Ready;
status = Color.Green; //error
“`

类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。
比较两个类类型的对象时,只有实例的成员会被比较。
静态成员和构造函数不在比较的范围内。

“`TypeScript
class Animal {
feet: number;
constructor(name: string, numFeet: number) { }
}

class Size {
feet: number;
constructor(numFeet: number) { }
}

var a: Animal;
var s: Size;

a = s; //OK
s = a; //OK
“`

类的私有成员

私有成员会影响兼容性判断。
当类的实例用来检查兼容时,如果它包含一个私有成员,那么目标类型必须包含来自同一个类的这个私有成员。
这允许子类赋值给父类,但是不能赋值给其它有同样类型的类。

泛型

因为TypeScript是结构性的类型系统,类型参数只影响使用其做为类型一部分的结果类型。比如,

“`TypeScript
interface Empty {
}
var x: Empty;
var y: Empty;

x = y; // okay, y matches structure of x
“`

上面代码里,x和y是兼容的,因为它们的结构使用类型参数时并没有什么不同。
把这个例子改变一下,增加一个成员,就能看出是如何工作的了:

“`TypeScript
interface NotEmpty {
data: T;
}
var x: NotEmpty;
var y: NotEmpty;

x = y; // error, x and y are not compatible
“`

在这里,泛型类型在使用时就好比不是一个泛型类型。

对于没指定泛型类型的泛型参数时,会把所有泛型参数当成any比较。
然后用结果类型进行比较,就像上面第一个例子。

比如,

“`TypeScript
var identity = function(x: T): T {
// …
}

var reverse = function(y: U): U {
// …
}

identity = reverse; // Okay because (x: any)=>any matches (y: any)=>any
“`

高级主题

子类型与赋值

目前为止,我们使用了兼容性,它在语言规范里没有定义。
在TypeScript里,有两种类型的兼容性:子类型与赋值。
它们的不同点在于,赋值扩展了子类型兼容,允许给any赋值或从any取值和允许数字赋值给枚举类型或枚举类型赋值给数字。

语言里的不同地方分别使用了它们之中的机制。
实际上,类型兼容性是由赋值兼容性来控制的甚至在implements和extends语句里。
更多信息,请参阅TypeScript语言规范.

.d.ts文件 介绍

当使用外部JavaScript库或新的宿主API时,你需要一个声明文件(.d.ts)定义程序库的shape。
这个手册包含了写.d.ts文件的高级概念,并带有一些例子,告诉你怎么去写一个声明文件。

指导与说明

流程

最好从程序库的文档开始写.d.ts文件,而不是代码。
这样保证不会被具体实现所干扰,而且相比于JS代码更易读。
下面的例子会假设你正在参照文档写声明文件。

命名空间

当定义接口(例如:“options”对象),你会选择是否将这些类型放进命名空间里。
这主要是靠主观判断 – 使用的人主要是用这些类型声明变量和参数,并且类型命名不会引起命名冲突,放在全局命名空间里更好。
如果类型不是被直接使用,或者没法起一个唯一的名字的话,就使用命名空间来避免与其它类型发生冲突。

回调函数

许多JavaScript库接收一个函数做为参数,之后传入已知的参数来调用它。
当为这些类型与函数签名的时候,不要把这个参数标记成可选参数。
正确的思考方式是“会提供什么样的参数?”,不是“会使用到什么样的参数?”。
TypeScript 0.9.7+不会强制这种可选参数的使用,参数可选的双向协变可以被外部的linter强制执行。

扩展与声明合并

写声明文件的时候,要记住TypeScript扩展现有对象的方式。
你可以选择用匿名类型或接口类型的方式声明一个变量:

匿名类型var

TypeScript
declare var MyPoint: { x: number; y: number; };

接口类型var

TypeScript
interface SomePoint { x: number; y: number; }
declare var MyPoint: SomePoint;

从使用者角度来讲,它们是相同的,但是SomePoint类型能够通过接口合并来扩展:

TypeScript
interface SomePoint { z: number; }
MyPoint.z = 4; // OK

是否想让你的声明是可扩展的取决于主观判断。
通常来讲,尽量符合library的意图。

类的分解

TypeScript的类会创建出两个类型:实例类型,定义了类型的实例具有哪些成员;构造函数类型,定义了类构造函数具有哪些类型。
构造函数类型也被称做类的静态部分类型,因为它包含了类的静态成员。

你可以使用typeof关键字来拿到类静态部分类型,在写声明文件时,想要把类明确的分解成实例类型和静态类型时是有用且必要的。

下面是一个例子,从使用者的角度来看,这两个声明是等同的:

标准版

TypeScript
class A {
static st: string;
inst: number;
constructor(m: any) {}
}

分解版

TypeScript
interface A_Static {
new(m: any): A_Instance;
st: string;
}
interface A_Instance {
inst: number;
}
declare var A: A_Static;

这里的利弊如下:

  • 标准方式可以使用extends来继承;分解的类不能。这可能会在未来版本的TypeScript里改变:是否允许任何的extends表达式
  • 都允许之后为类添加静态成员
  • 允许为分解的类再添加实例成员,标准版不允许
  • 使用分解类的时候,为成员起合理的名字

命名规则

一般来讲,不要给接口加I前缀(比如:IColor)。
类为TypeScript里的接口类型比C#或Java里的意义更为广泛,IFoo命名不利于这个特点。

例子

下面进行例子部分。对于每个例子,先是使用使用方法,然后是类型声明。
如果有多个好的声明表示方法,会列出多个。

参数对象

使用方法

TypeScript
animalFactory.create(“dog”);
animalFactory.create(“giraffe”, { name: “ronald” });
animalFactory.create(“panda”, { name: “bob”, height: 400 });
// Invalid: name must be provided if options is given
animalFactory.create(“cat”, { height: 32 });

类型

TypeScript
namespace animalFactory {
interface AnimalOptions {
name: string;
height?: number;
weight?: number;
}
function create(name: string, animalOptions?: AnimalOptions): Animal;
}

带属性的函数

使用方法

TypeScript
zooKeeper.workSchedule = “morning”;
zooKeeper(giraffeCage);

类型

TypeScript
// Note: Function must precede namespace
function zooKeeper(cage: AnimalCage);
namespace zooKeeper {
var workSchedule: string;
}

可以用new调用也可以直接调用的方法

使用方法

TypeScript
var w = widget(32, 16);
var y = new widget(“sprocket”);
// w and y are both widgets
w.sprock();
y.sprock();

类型

“`TypeScript
interface Widget {
sprock(): void;
}

interface WidgetFactory {
new(name: string): Widget;
(width: number, height: number): Widget;
}

declare var widget: WidgetFactory;
“`

全局的/不清楚的Libraries

使用方法

TypeScript
// Either
import x = require(‘zoo’);
x.open();
// or
zoo.open();

类型

“`TypeScript
namespace zoo {
function open(): void;
}

declare module “zoo” {
export = zoo;
}
“`

外部模块的单个复杂对象

使用方法

TypeScript
// Super-chainable library for eagles
import eagle = require(’./eagle’);
// Call directly
eagle(‘bald’).fly();
// Invoke with new
var eddie = new eagle(1000);
// Set properties
eagle.favorite = ‘golden’;

类型

“`TypeScript
// Note: can use any name here, but has to be the same throughout this file
declare function eagle(name: string): eagle;
declare namespace eagle {
var favorite: string;
function fly(): void;
}
interface eagle {
new(awesomeness: number): eagle;
}

export = eagle;
“`

回调函数

使用方法

TypeScript
addLater(3, 4, x => console.log('x = ’ + x));

类型

TypeScript
// Note: ‘void’ return type is preferred here
function addLater(x: number, y: number, (sum: number) => void): void;

如果你想看其它模式的实现方式,请在这里留言!
我们会尽可能地加到这里来。


  1. A-Za-z ↩︎

  2. A-Za-z ↩︎

  3. 0-9 ↩︎

  4. 0-9 ↩︎

  5. 0-9 ↩︎

  6. 0-9 ↩︎

  7. A-Za-z ↩︎

  8. 0-9 ↩︎

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值