TypeScript进阶玩法

Posted on August 1, 2015

本来这篇文章准备写一下TypeScript的各种语法特性。发现N神的那篇博客《从 ActionScript3 到 TypeScript》好像又更新了,对TS的语法覆盖已经挺全的了。那这篇文章就接着N神的博客,写一下部分进阶部分的内容吧。另外,推荐从ActionScript3转到TypeScript上的童鞋,同时可以看一下这篇关于JavaScript的教程,对理解TypeScript也非常有帮助:《JavaScript秘密花园》。

类型推理

对于从AS3转过来的童鞋,包括我,一开始可能都不太熟悉类型推理这个功能,还强迫症似的把每个变量的类型都显式写上。但在使用了类型推理功能后一段时间,我深深感受到了这个功能的强大,再也不愿意回到没有类型推理的语言里了。现在各种现代语言的编译器都已经支持类型推理,当然也包括TypeScript的编译器。类型推理简单说就是编译器提供的一种语法便利,能够让你可以不用声明变量的类型,但是并不会影响强类型的判断,而是在第一次对变量赋值的时候,编译器会自动推理出这个变量的类型。从而让你写出更加简洁的代码。如下面的例子:

var display = new Shape();
display.x = 10; //编译通过
display.getChildAt(0); //编译报错,display上不存在getChildAt()方法。

如果在AS3中,我们必须显式声明变量类型:var display:Shape = new Shape(),否则这个变量就会被当成Object类型,跳过所有编译检查。而在TypeScript中,编译器能够智能推理出来这个display的类型就是Shape,而不需要你显式声明。然后根据推理出的类型,做强类型的编译检查。当然,你显式声明也是没问题的。但是建议尽可能不写,当你在重构代码的时候,就会发现好处。例如,我现在要改掉Shape为Sprite,只需要改new后面的关键字这一处。如果你同时显式声明了类型,还得再改一遍类型。所以类型推理不仅可以让写法更加简洁,对代码维护其实也更加有优势。

另外,并不是所有的情况,类型推理都能生效。在少量类型推理无法生效的情况下,例如实例化一个变量,但是不立即赋初始值。这种情况下建议还是加上显式类型声明。通常一条简单的规则来判断是否需要加显式类型声明,就是你访问这个变量,看代码提示是否正确。若不正确,那就是类型推理错误了。这时候加上显式的类型声明。但是绝大部分情况下,我们都可以使用非常简洁的编码习惯。

强类型函数变量

很多刚接触TypeScript的人,还在按着AS3的编程方式写着代码,可能并没有体会到这点:TypeScript其实是比AS3更加强类型的语言,它能够描述的强类型粒度更小更精确。大家一定都定义过回调函数,举一个最典型的例子:事件监听。 在AS3中是这么定义的:

addEventListener(type:String, listener:Function, ...args):void

而在TypeScript中可以这么定义:

addEventListener(type:string, listener:(event:Event)=>void, ...args):void

这里我们忽略其他参数,只关注listener参数,应该很容易看出这两者的区别。在AS3中,函数只能被声明为Function类型,对编译器来说,也就无法立即检查函数的参数和返回值是否匹配。如果不匹配,必须到运行时才会报错。而在TypeScript中,可以直接使用箭头定义方式,描述一个函数变量的具体类型,包括参数和返回值。这样如果监听错了一个函数,编译器在语法提示阶段就能直接告诉你,节省了很多调试的时间。另外你也不再需要特地在注释中注明回调函数的参数和返回值,强类型的定义过了,一目了然。

复合类型

复合类型同样是TypeScript提供的更小粒度的强类型描述方式。

在AS3中,如果遇到一个成员变量要同时接受两种或两种以上类型的值,我们通常会怎么做?第一种方案:声明一个接口,然后让变量类型定义为这个接口,并让每种类型都去实现这种接口。挺麻烦的吧,而且对基本数据类型比如number之类的,这种方案就无效了。所以我们通常都会直接采用第二种方案:声明变量类型为Object或者*号(在TypeScript对应为:any),就是不指定类型。这样编译器就不检查它了。

而在TypeScript中,我们可以用更简单且有效的方式,来避免这种无法描述的情况,就是使用复合类型:

class Image {
    public source:BitmapData|string;
}
这里我们在Image类中定义了一个成员变量source,希望它同时能接受两种类型的数据值,BitmapData和string字符串。那么类型就直接声明为BitmapData string即可。如果你试图对这个属性赋值其他类型的数据,编译器将会给你报一个错。而如果声明为any,将不做任何类型检查。用好复合类型显然能够更大程度降低bug出现概率。以上的例子是两种类型符合的情况,如果两种以上呢?继续用竖线分隔即可:BitmapData string number。

接口Interface

TypeScript中的接口跟AS3中的接口不太一样,代表的含义更加广泛,可以说是无处不在。它不仅仅指使用interface关键字定义的区块,还包括用一对{}括号定义的部分。来看一个例子:

function handle(data:{id:number;label:string;}):void{
    //doing something here...
}

以上定义了一个名为handle的函数,参数只有一个data,我们要关注的是data的类型:{id:string;label:string;},{}括号内表示的就是一个接口。具体含义是:传入的data对象必须符合此接口定义,也就是data上必须包含id,和label两个属性,并且属性类型分别是number和string。这个例子可能还不够明显,我们进一步改进一下,以上的函数例子等价于:

interface Data {
    id:number;
    label:string;
}

function handle(data:Data):void{
    //doing something here...
}

这种接口定义方式,同样也可以声明在成员变量上。像这样只使用一次的接口,采用{}方式定义能够非常方便,并且也能达到类型检查的目的,比声明为any方式要好的多。从这里也能看出TypeScript的强类型描述粒度是非常小而精确的。 在AS3中,我们的接口只能定义成员变量和成员方法。而在TypeScript中,接口还可以定义构造函数的参数类型:

var eventClass:{new (type:string): Event;};
var event = new eventClass(type); //event的类型推理结果为Event。

这里我们定义了一个变量eventClass,它的类型是一个接口:{new (type:string): Event;}。这个接口表示,变量类型是一个类定义,并且这个类的构造函数里必须含有type:string这个参数。随后我们可以直接使用new关键字来实例化这个类定义。 另外一点跟AS3不同的是,在TypeScript中,编译器判断一个对象是否实现某个接口,并不需要这个对象的类显式使用implements关键字实现接口。以上面的函数为例,我们可以直接var data = {id:1,label:”content”},然后将这个data传入handle()作为参数是能通过编译的。

泛型

TypeScript中同样也支持泛型,泛型让我们能够定义一种动态的类型,结合类型推理使用时将会非常方便。同样的,先来看一个例子:

function create<T>(EventClass:{new (type:string): T;}, type:string):T {
    var event = new EventClass(type);
    return event;
}

var event = create(TimerEvent,"timer"); //event的类型推理结果为TimerEvent。

这里我们定义了一个create()函数,这个函数有两个参数,第一个参数EventClass在上一节中已经介绍的含义,这里唯一要注意的就是它的构造函数返回值是模板T而不是固定的Event。这整个函数要实现的作用是:传入一个事件类定义,实例化它并返回这个事件类的实例。在AS3中,我们必须显示声明一个函数的返回值。而这里的create()返回类型是动态的。直接得到TimerEvent,而不是父类Event。这样可以避免做类型转换。这个特性在设计对象池方法时非常有用。更多的关于泛型的资料。可以参考微软官方的handbook,写的相当详细:http://www.typescriptlang.org/Handbook#generics-hello-world-of-generics

常量枚举

在N神的博客里已经提到了enum枚举对象。这里更进一步,介绍一下常量枚举,写法很简单,就是在enum关键字前再加一个const:

const enum Keys {
    scaleX,
    scaleY,    
    rotation   
}

这里的枚举定义,因为没有显式指定每个枚举量对应的数字,所以它会从0开始自动分配。例如访问Keys.scaleX,等价于数字0。常量枚举就是简单加一个const,但是作用却相当大。它能够让你正常使用代码提示访问Keys.scaleX,但当最终编译为JS代码时,这个Keys.scaleX会自动被替换为数字0。这样不仅会有更高的运行性能,也能大幅度减少生成js的代码量。

文本模板

文本模板的功能也非常实用,对于多行文本,我们直接写在代码里的话,通常要在换行的地方手动改成\n,并且头尾加上”+”符号,操作非常麻烦。而TypeScript里是支持文本模板功能的,可以用一对·符号(波浪线~那个键)将整段文本头尾包含起来,而不需要手动修改换行,看一个例子就明白了:

var name = "contentDisplay";
var text = "<s:Label id=\""+name+"\" fontFamily=\"Tahoma\" fontSize=\"22\"\n"+
 "textColor=\"0x727070\" left=\"1\" right=\"1\" top=\"36\" bottom=\"45\"\n"+
 "verticalAlign=\"middle\" textAlign=\"center\" text=\"contentDisplay\"/>"

以上代码等价为:

var name = "contentDisplay";
var text = `<s:Label id="${name}" fontFamily="Tahoma" fontSize="22"
 textColor="0x727070" left="1" right="1" top="36" bottom="45"
 verticalAlign="middle" textAlign="center" text="contentDisplay"/>`