转自 zhongsp.gitbooks.io
为了让程序有价值,我们需要能够处理最简单的数据单元:数字,字符串,结构体,布尔值等。
TypeScript支持与JavaScript几乎相同的数据类型,此外还提供了实用的枚举类型方便我们使用。
最基本的数据类型就是简单的true/false值,在JavaScript和TypeScript里叫做boolean
(其它语言中也一样)。
var isDone: boolean = false;
和JavaScript一样,TypeScript里的所有数字都是浮点数。
这些浮点数的类型是number
。
除了支持十进制和十六进制字面量,Typescript还支持ECMAScript 2015中引入的二进制和八进制字面量。
var decLiteral: number = 6;
var hexLiteral: number = 0x9837abdef;
var binaryLiteral: number = 0b0010;
var octalLiteral: number = 0o74563;
JavaScript程序的另一项基本操作是处理网页或服务器端的文本数据。
像其它语言里一样,我们使用string
表示文本数据类型。
和JavaScript一样,可以使用双引号("
)或单引号('
)表示字符串。
var name: string = "bob";
name = "smith";
你还可以使用模版字符串,它可以定义多行文本和内嵌表达式。
这种字符串是被反引号包围(`
),并且以${ 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.`;
这与下面定义sentence
的方式效果相同:
var sentence: string = "Hello, my name is " + name + ".\n\n" +
"I'll be " + (age + 1) + " years old next month.";
TypeScript像JavaScript一样可以操作数组元素。
有两种方式可以定义数组。
第一种,可以在元素类型后面接上[]
,表示由此类型元素组成的一个数组:
var list: number[] = [1, 2, 3];
第二种方式是使用数组泛型,Array<元素类型>
:
var list: Array = [1, 2, 3];
enum
类型是对JavaScript标准数据类型的一个补充。
像C#等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字。
enum Color {Red, Green, Blue};
var c: Color = Color.Green;
默认情况下,从0
开始为元素编号。
你也可以手动的指定成员的数值。
例如,我们将上面的例子改成从1
开始编号:
enum Color {Red = 1, Green, Blue};
var c: Color = Color.Green;
或者,全部都采用手动赋值:
enum Color {Red = 1, Green = 2, Blue = 4};
var c: Color = Color.Green;
枚举类型提供的一个便利是你可以由枚举的值得到它的名字。
例如,我们知道数值为2,但是不确定它映射到Color里的哪个名字,我们可以查找相应的名字:
enum Color {Red = 1, Green, Blue};
var colorName: string = Color[2];
alert(colorName);
有时,我们可能会想要为那些在编写程序阶段还不清楚其类型的变量指定一个类型。
这些值可能来自于动态的内容,比如来自用户或第三方代码库。
这种情况下,我们不希望类型检查器对这些值进行检查或者说让它们直接通过编译阶段的检查。
那么我们可以使用any
类型来标记这些变量:
var notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean
在对现有代码进行改写的时候,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'.
当你只知道数据的类型的一部分时,any
类型也是有用的。
比如,你有一个数组,它包含了不同的数据类型:
var list: any[] = [1, true, "free"];
list[1] = 100;
某种程度上来说,void
类型像是与any
类型相反,它表示没有任何类型。
当一个函数没有返回值时,你通常会见到其返回值类型是void
:
function warnUser(): void {
alert("This is my warning message");
}
声明一个void
类型的变量没有什么大用,因为你只能为它赋予undefined
和null
:
var unusable: void = undefined;
TypeScript的核心原则之一是对值所具有的shape进行类型检查。
它有时被称做“鸭式辨型法”或“结构性子类型化”。
在TypeScript里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。
下面通过一个简单示例来观察接口是如何工作的:
function printLabel(labelledObj: { label: string }) {
console.log(labelledObj.label);
}
var myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);
类型检查器会查看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);
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"});
带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个?
符号。
可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误。
比如,我们故意将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"});
接口能够描述JavaScript中对象拥有的各种各样的外形。
除了描述带有属性的普通对象外,接口也可以描述函数类型。
为了使用接口表示函数类型,我们需要给接口定义一个调用签名。
它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。
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): boolean {
var result = src.search(sub);
if (result == -1) {
return false;
}
else {
return true;
}
}
函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。
如果你不想指定类型,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;
}
}
与使用接口描述函数类型差不多,我们也可以描述数组类型。
数组类型具有一个index
类型表示索引的类型,还有一个相应的返回值类型表示通过索引得到的元素的类型。
interface StringArray {
[index: number]: string;
}
var myArray: StringArray;
myArray = ["Bob", "Fred"];
支持两种索引类型:string和number。
数组可以同时使用这两种索引类型,但是有一个限制,数字索引返回值的类型必须是字符串索引返回值的类型的子类型。
索引签名能够很好的描述数组和dictionary
模式,它们也要求所有属性要与返回值类型相匹配。
因为字符串索引表明obj.property
和obj["property"]
两种形式都可以。
下面的例子里,length
的类型与字符串索引类型不匹配,所以类型检查器给出一个错误提示:
interface NumberDictionary {
[index: string]: number;
length: number; // 可以,length是number类型
name: string // 错误,`name`的类型不是索引类型的子类型
}
与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) { }
}
接口描述了类的公共部分,而不是公共和私有两部分。
它不会帮你检查类是否具有某些私有成员。
当你操作类和接口的时候,你要知道类是具有两个类型的:静态部分的类型和实例的类型。
你会注意到,当你用构造器签名去定义一个接口并试图定义一个类去实现这个接口时会得到一个错误:
interface ClockConstructor {
new (hour: number, minute: number);
}
class Clock implements ClockConstructor {
currentTime: Date;
constructor(h: number, m: number) { }
}
这里因为当一个类实现了一个接口时,只对其实例部分进行类型检查。
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);
因为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;
一个接口可以继承多个接口,创建出多个接口的合成接口。
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;
先前我们提过,接口能够描述JavaScript里丰富的类型。
因为JavaScript其动态灵活的特点,有时你会希望一个对象可以同时具有上面提到的多种类型。
一个例子就是,一个对象可以同时做为函数和对象使用,并带有额外的属性。
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
var c: Counter;
c(10);
c.reset();
c.interval = 5.0;
在使用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() { }
}
在上面的例子里,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");
如果你使用过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);
这个例子展示了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.
在上面的例子里,我们可以自由的访问程序里定义的成员。
如果你对其它语言中的类比较了解,就会注意到我们在之前的代码里并没有使用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.`);
}
}
private
当成员被标记成private
时,它就不能在声明它的类的外部访问。比如:
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
new Animal("Cat").name; // Error: 'name' is private;
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
这个例子中有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
注意,我们不能在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.`);
}
}
注意看我们是如何舍弃了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);
}
我们可以随意的设置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);
}
我们可以修改一下密码,来验证一下存取器是否是工作的。当密码不对时,会提示我们没有权限去修改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}));
当你在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());
在这里,我们写了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());
上面的代码里,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());
这个例子里,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};
关于术语的一点说明:
必须要注意一点在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
和使用被引号括住的名字定义的。
// In a .d.ts file or .ts file that is not a module:
declare module "SomeModule" {
export function fn(): string;
}
///
import m = require("SomeModule");
这里的引用标签指定了外来模块的位置。
这就是一些Typescript例子中引用node.d.ts的方法。
如果你想把命名空间转换为模块,它可能会像下面这个文件一件:
export namespace Shapes {
export class Triangle { /* ... */ }
export class Square { /* ... */ }
}
顶层的模块Shapes
包裹了Triangle
和Square
。
这对于使用它的人来说是让人迷惑和讨厌的:
import shapes = require('./shapes');
var t = new shapes.Shapes.Triangle(); // shapes.Shapes?
TypeScript里模块的一个特点是不同的模块永远也不会在相同的作用域内使用相同的名字。
因为使用模块的人会为它们命名,所以完全没有必要把导出的符号包裹在一个命名空间里。
再次重申,不应该对模块使用命名空间,使用命名空间是为了提供逻辑分组和避免命名冲突。
模块文件本身已经是一个逻辑分组,并且它的名字是由导入这个模块的代码指定,所以没有必要为导出的对象增加额外的模块层。
下面是改进的例子:
export class Triangle { /* ... */ }
export class Square { /* ... */ }
import shapes = require('./shapes');
var t = new shapes.Triangle();
就像每个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 = /^[A-Za-z]+$/;
var numberRegexp = /^[0-9]+$/;
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);
}
});
随着更多验证器的加入,我们需要一种手段来组织代码,以便于在记录它们的类型的同时还不用担心与其它对象产生命名冲突。
因此,我们把验证器包裹到一个命名空间内,而不是把它们放在全局命名空间下。
下面的例子里,把所有与验证器相关的类型都放到一个叫做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 }`);
}
});
当应用变得越来越大时,我们需要将代码分离到不同的文件中以便于维护。
现在,我们把Validation
命名空间分割成多个文件。
尽管是不同的文件,它们仍是同一个命名空间,并且在使用的时候就如同它们在一个文件中定义的一样。
因为不同文件之间存在依赖关系,所以我们加入了引用标签来告诉编译器文件之间的关联。
我们的测试代码保持不变。
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}
}
///
namespace Validation {
const lettersRegexp = /^[A-Za-z]+$/;
export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}
}
///
namespace Validation {
const numberRegexp = /^[0-9]+$/;
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);
}
});
当涉及到多文件时,我们必须确保所有编译后的代码都被加载了。
我们有两种方式。
第一种方式,把所有的输入文件编译为一个输出文件,需要使用--out
标记:
tsc --outFile sample.js Test.ts
编译器会根据源码里的引用标签自动地对输出进行排序。你也可以单独地指定每个文件。
tsc --outFile sample.js Validation.ts LettersOnlyValidator.ts ZipCodeValidator.ts Test.ts
第二种方式,我们可以编译每一个文件(默认方式),那么每个源文件都会对应生成一个JavaScript文件。
然后,在页面上通过标签把所有生成的JavaScript文件按正确的顺序引进来,比如: