TypeScript语言学习参考

邹业盛 2015-11-18 00:00 更新
  1. 概述与环境安装
  2. 声明与预置规则
  3. 类型
  4. 操作符
  5. 控制结构
  6. 函数,声明,参数约束与默认值
  7. 名字空间与模块加载
  8. 开发调试
  9. 总结

本文的内容对应 TypeScript 的 1.6.2 版本。

1. 概述与环境安装

TypeScript 是在 JavaScript 基础上被微软做出来的新语言,目的是补充 js 的语法,而变更的地方主要是有两点。一是加入了类型声明,二是加入了常见的基于“类”的面向对象语法。前者,“类型声明”使得代码的静态分析成为可能,后者则为组织 js 代码提供了一种更易于被人接受的形式。

说得更具体一点的话,就是 TypeScript 以“为了更容易维护与合作”,在语法上给 JavaScript 添加了一些明确约束和通用的面向对象能力。比如,你可以明确声明变量的存在(在当前文件中没有,但是你自己清楚它在运行时是没有问题的,比如 document 在浏览器环境中),明确声明变量的类型(之后 TypeScript 的静态分析能力就会配合 IDE 的功能作代码检查了),还有声明函数返回类型,函数参数类型,典型的类,继承,私有方法,等等等等,也许你在其它语言中早已熟悉的东西。

TypeScript 目前可以看成是 js 的预处理方案,使用 ts 写的代码,通过编译器得到 js 代码,这里说的“编译”仅仅是在“当前文件”中对代码结构作变换,像变量名什么的是不会被更改的。

使用 npm 可以直接完成 TypeScript 的安装:

sudo npm install -g typescript

安装完成之后,在 node 的 bin 目录下,会有一个 tsc ,这个就是 TypeScript 的编译工具,使用它可以从一个 ts 文件编译得到一个 js 文件:

tsc hello.ts

这样,就可以在同目录中得到一个 hello.js

编译工具还有其它的一些选项,具体的直接敲入 tsc 后看屏幕信息吧。

2. 声明与预置规则

不管是从严谨,甚至是合理的角度,我个人认为代码中是不应该存在“无源之水”之类的东西,代码中的每一样东西应该都明确的知道从何而来。退一步说,也应该有一个统一的“全局空间”,“默认空间”之类的概念。不过这东西在 js 环境中确实是有些乱的。语言层面全局中即有 parseInt 这种函数,也有 Date 这种对象,还有 Math 这种名字空间?同时典型浏览器环境暴露在全局中的还有 documentXMLHttpRequest 这些。

当你在一个空白的文件中,写了一句:

var dt = new Date()

这样的代码的时候, ts 怎么知道 Date 是什么?或者换一个,写了一句:

$.ajax({});

ts 又怎么知道 $ 是什么?

它当然不知道,所以,按理说,这种情况下,语法检查就会抛出“未定义的引用”这种错误。但是事实上,上面的代码拿到浏览器中,在加载了相关的资源文件之后,是可以正常运行的。

面对这种问题, ts 有一种 Ambient Declarations 的机制,用于显示声明一个外部的“我知道的东西”。

declare var $;
$.ajax({});

加上 declare 这句, ts 的语法检查就知道 $ 是什么东西了(但它不管实际运行时它怎么来)。

实际使用时,其实不加 declare ,对于 Datedocument 这些全局对象 ts 也“认识”的,因为在 ts 的机制中,已经预定义了一套声明,在安装目录的 /lib/lib.d.ts 文件中,里面包含了浏览器 API 和 js 语言层面的已有对象。

3. 类型

这里提到的“类型”包含了两层意义,一方面表示“一个变量是什么东西”,比如 var a = '1'; ,我们可以说这个 a 是一个字符串。另一方面,表示“如何描述一个变量是什么东西”,举个例子:

var a:string[];

我们声明了, a 是一个由数字串组成的列表。但是,如果我们想进一步声明: a 不光是一个列表,而且它的第一个成员是字符串,第二个成员是数字,这种情况光是 string[] 就不够了。这时需要写成:

var a:[string, number];

上面的 string[][string, number] 在 ts 中其实是两种不同的类型,前者是 Array ,后者是 Tuple 。但是,这两个东西的区别,其实只在“声明”时有,或者说区别只在于“约束”,而不在于实际的数据形式。即反过来看,你无法区别 ['1', '1'] 这个值的类型,它对应的到底是 string[] 这个 Array ,还是 [string, string] 这个 Tuple ,当然也可以说它两者都是,从这个角度来说,类型的意义,也仅仅是在于“约束”,跟实际的数据结构完全没有关系的(js 这种高高级的语言本来也不关心)。想明白这点,应该有助于减少纠结。

前面的一点点例子,也展示了 ts 中的类型的用法——后置声明。

3.1. 基本类型

var a:number = 1;
var b:string = 'abc';
var c:boolean = true;
var d:void;
var e:any = null;

enum Color { red = 5, blue = 2, green = 3 }
var f:Color = Color.red;

3.2. 函数声明

var func1 = function(a:string, b:number):number{
    return 1;
};

function func2(a:string, b:number):number{
    return 1;
}

function func3(a:string, b:number):void{
}

参数的类型声明,一样是放参数名后面。在参数部分之后,可以加上函数返回值的类型声明。

3.3. 类型聚合

在声明类型的时候,类型与类型之间,是可以有逻辑关系的,比如“A类型,或者B类型”,甚至是“A类型,也是B类型”。推广一下,就是指定的对象实现了多个接口中的一个,或者,实现了所有的接口。

function f(a:number|string):string|number{
    return a;
}

4. 操作符

整体上跟 js 是一样的,因为引入了类型,提几点:

function f():void{
    var obj = {a: 1};
    return (delete obj.a);
}

上面的代码有编译期错误,因为 delete 操作符的返回值是 boolean 类型。

void 也是一个操作符,它的返回值是 undefined

function f():void{
    var obj = {a: 1};
    return (void (delete obj.a));
}

这样就对了。

4.1. 类型求值

typeof 这个操作符,本来是返回一个字符串。但是在 ts 中,如果它的使用位置是在一个“类型声明”中,则它的行为就是一个“类型求值”:

var n = 123;
function f():typeof n{
    return (typeof n);
}

上面的代码就是错的,因为 f 的返回值需要是一个 number ,但是实际上返回了一个 string

4.2. 匹配赋值

(这部分功能 ECMA 6 以上本来就支持了,ts 便不会再作额外转换)

在 ts 中可以写:

var x:number, y:number;
[x, y] = [1, 2, 3];
console.log(x, y);

ts 会生成这样的 js 代码:

var x, y;
_a = [1, 2, 3], x = _a[0], y = _a[1];
console.log(x, y);
var _a;

再扩展一下,可以给一个“默认值”:

var [x, y, z='ok'] = [1, 2, undefined];
console.log(x, y, z);

注意, z 在这里的行为是,当对应位置没有匹配时,取 ok 。但是,如果写成:

var [x, y, z='ok'] = [1, 2];

这样就不行,会检查出类型不匹配。

前面正确的例子对应生成的 js 代码是:

var _a = [1, 2], x = _a[0], y = _a[1], _b = _a[2], z = _b === void 0 ? 'ok' : _b;
console.log(x, y, z);

列表可以匹配,对象也可以匹配,形式上是把原始属性名映射到目标属性名:

var {x: a, y: b, z: c=false} = {x: 1, y: 2, z: undefined};
console.log(a, b, c);

生成的 js 代码是:

var _a = { x: 1, y: 2, z: undefined }, a = _a.x, b = _a.y, _b = _a.z, c = _b === void 0 ? false : _b;
console.log(a, b, c);

5. 控制结构

(没有特殊的地方, ECMA 6 新增的语法不论)

6. 函数,声明,参数约束与默认值

6.1. 参数匹配与默认值

前面说类型的时候,谈到过一点函数的定义形式:

var func1 = function(a:string, b:number):number{
    return 1;
};

结合前面“匹配赋值”,函数在定义时就可以有“默认值”机制了:

function f({x: a=1}){
    console.log(a);
}

f({});

上面的代码从传入的对象中抽取 x 属性赋值给 a 变量,如果没有 x 属性,则把 a 变量的值赋值成 1 ,对应的 js 代码为:

function f(_a) {
    var _b = _a.x, a = _b === void 0 ? 1 : _b;
    console.log(a);
}
f({});

还可以复杂一点:

function f({x: a=1, pair: [x, y]=['a', 'b']}){
    console.log(x, y);
}

f({pair: ['1', '2']});

6.2. 参数类型约束

前面在函数的参数中作了额外的事的话,类型约束上就没法直接做了。这种情况,就需要在整个参数对象上,定义一个 接口 来约束类型。

interface FInfo {
    x?: number;
    pair: [string, string];
}

function f({x: a=1, pair: [x, y]=['a', 'b']}: FInfo){
    console.log(x, y);
}

f({pair: ['1', '2']});

定义函数时,直接声明参数是一个 FInfo 类型,那么在编译期就可以用参数的有效性检查了。( interface 只是约束,只在编译期起作用,对生成 js 代码没有任意影响)。

类型本身是可以有逻辑关系的,这个前面提过:

function f(a:number|string):string|number{
    return a;
}

如果一个参数可以缺失,可以使用这种方式声明,加一个问号:

function f(a?:number|string):string|number{
    return a;
}

6.3. 新的函数书写形式

ts 为函数表达式提供了一种更简单的写法,比如原来要写成:

var f = function(a, b, c){return a}

现在 ts 中可以写为:

var f = (a, b, c) => a;

使用 => 箭头的语法,来作为一个函数表达式。

箭头的左边,是参数。右边,是函数体,左右边都有简写形式。上面的右边就简写了,完整的形式应该是:

var f = (a, b, c) => { return a };

当参数只有一个时,左边的括号可以省略:

var f = a => a+1;
//function(a){ return a+1 }

如果没有参数,则写为一个空括号:

var f = () => 1;
//function(){return 1}

注意,箭头形式的函数表达式中, this 不是上下文,会被 ts 处理成一个简单的闭包变量。

var f = () => this.x;

转成 js 是:

var _this = this;
var f = function () { return _this.x; };

箭头形式的函数表达式用在回调函数中很方便:

var n = ([1,2,3]).forEach((x) => x+1);
console.log(n);

生成的 js 是:

var n = ([1, 2, 3]).forEach(function (x) { return x + 1; });
console.log(n);

带上类型也是一样的:

var f = (x:number):number => x + 1;

7. 类

7.1. 构造函数,实例化

ts 中有标准的 class 关键词了,定义一个类的方式变得直观:

enum Sex {male, female};
class People {
    name: string = 'noname';
    sex: Sex;

    constructor(name:string, sex: Sex){
        this.name = name;
        this.sex = sex;
    }

    get_name():string{
        return this.name;
    }

    set_name(name:string):People{
        this.name = name;
        return this;
    }

    get_sex():Sex{
        return this.sex;
    }

    set_sex(sex:Sex):People{
        this.sex = sex;
        return this;
    }

}

var p = new People('hello', Sex.male);
console.log(p.get_name());
console.log(p.set_name('world').get_name());

上面的代码就是一个典型的“类”的定义,里面有它的属性和方法。我们还额外定义了一个枚举类型。

这里在写法上的注意的是,“属性”结尾,可选一个分号结尾。“方法”结尾,一定不能要分号。

constructor 方法,强制为类的构造方法。 new 用于实例化一个类。

一个类的方法实现中, this ,仍然是 js 中的“上下文”,所以对于上述代码,我们写:

var f = p.get_name;
console.log(f());

输出的结果,是 undefined

为此,我们可以使用“属性”来代替“方法”,本来“函数”在 js 中就跟数字,字符串一样,是再普通不过的类型嘛。使用“箭头函数”把 this 作为闭包变量使用,我们把前面的代码修改一下:

class People {
    name: string = 'noname';
    sex: Sex;

    get_real_name = ():string => {return this.name};
    set_real_name = (name:string):People => {
        this.name = name;
        return this;
    };
    
    ... ...

这样,再使用:

var get = p.get_real_name;
var set = p.set_real_name;
console.log(get());
set('real');
console.log(get());

结果就不会有任何改变了。

前面的 People 这个类定义,转换出来的 js 代码是:

var People = (function () {
    function People(name, sex) {
        var _this = this;
        this.name = 'noname';
        this.get_real_name = function () { return _this.name; };
        this.set_real_name = function (name) {
            _this.name = name;
            return _this;
        };
        this.name = name;
        this.sex = sex;
    }
    People.prototype.get_name = function () {
        return this.name;
    };
    People.prototype.set_name = function (name) {
        this.name = name;
        return this;
    };
    People.prototype.get_sex = function () {
        return this.sex;
    };
    People.prototype.set_sex = function (sex) {
        this.sex = sex;
        return this;
    };
    return People;
})();

7.2. 继承

ts 中使用 extends 关键词来做继承。

class A {
    constructor(name:string=''){}
}
class B extends A {
    name:string = 'B';
}

var b = new B();
console.log(b.name);

ts 中的 extends 不支持多重继承。

7.3. 成员类型

ts 中的类的成员可以有 publicprivate 两类限定。默认是 public (其实还有一个 protected 类型,在后面的“父类引用”中会介绍)。

class A {
    private _name:string;
    public get_name = () => this._name;
    constructor(name:string){
        this._name = name;
    }
}

var a = new A('hello');
//console.log(a._name); //ERROR
console.log(a.get_name());

对于 publicprivate ,在构造函数上有一点“特殊功能”,可以在构造函数的参数中声明 publicprivate ,这样的话,实例化的时候会自动处理相应的实例成员变量。

class A {
    constructor(){ }
}

var a = new A();
console.log(a.name);

上面的代码在编译时会报 a.name 不存在的错误。改一下:

class A {
    constructor(public name:string='hello'){ console.log('here') }
}

var a = new A();
console.log(a.name);

在构造函数的参数前面多加一个 public ,那么在实例化的时候, name 参数会自动对应到对实例的 name 成员属性的赋值上。上面的 ts 代码生成的 js 代码是:

var A = (function () {
    function A(name) {
        if (name === void 0) { name = 'hello'; }
        this.name = name;
        console.log('here');
    }
    return A;
})();
var a = new A();
console.log(a.name);

生成的 function A(name) 中,第一行是处理默认参数行为。第二行,就是编译生成的“自动对应同名属性”的行为。

换成 private 也是一样的:

class A {
    get_name = ():string => this.name;
    constructor(private name:string='hello'){ console.log('here') }
}

var a = new A();
console.log(a.get_name());

7.4. 静态成员,类引用

前面写在“类”中的属性或方法,可以看成是“实例的成员”,相对的,我们在前面加一个 static 限定的话,就可以把属性或方法定义成“静态的成员”,或者从认识上是“类成员”。这些属性或方法存在于类的名字名字空间下,与实例无关。

class A {
    x:number = 0;
    static x:number = 1;
}

var a = new A();
console.log(a.x);
console.log(A.x);

上面代码中,虽然有两个 x ,但是一个是实例属性,一个是“类属性(静态属性)”,所以,它们是两个不同,也互不相关的东西。

对应生成的 js 代码是:

var A = (function () {
    function A() {
        this.x = 0;
    }
    A.x = 1;
    return A;
})();
var a = new A();
console.log(a.x);
console.log(A.x);

在实例的相关成员定义中,要引用静态成员的话:

class A {
    x:number = 0;
    static x:number = 1;
    show_normal = ():number => this.x;
    show_static = ():number => A.x;
}

直接写 A.x 自然没有问题,但是更好的方法,应该是表达成“当前实例的类”,在 js 中,“类”其实就是一个函数,比如上面的代码转成 js 是:

var A = (function () {
    function A() {
        var _this = this;
        this.x = 0;
        this.show_normal = function () { return _this.x; };
        this.show_static = function () { return A.x; };
    }
    A.x = 1;
    return A;
})();

我们可以通过对应实例的 constructor 成员来得到这个 A 的引用:

class A {
    x:number = 0;
    static x:number = 1;
    static name:string = '123';
    show_normal = ():number => this.x;
    show_static = ():number => A.x;
    show_static2 = ():number => this.constructor.x;
    show_cls_name = ():string => this.constructor.name;
}

var a = new A();
console.log(a.show_normal());
console.log(a.show_static());
console.log(a.show_static2());
console.log(a.constructor === A);
console.log(a.show_cls_name());
console.log(A.name);

好吧,我自认为的道理都说完了,不过,上面的代码会提示编译有错误( this.constructor 这个函数没有 x 这个属性),但是却能正确得到 js 结果。同时,那个静态的 name ,赋值为 123 也是没用的,得到的结果总是 A 。不要问我为什么,我也不知道。

接下来要确认的问题,静态成员可以被更改吗?或者说得准确点, ts 允许静态成员被更改吗?因为 js 层面肯定是没问题的。

class A {
    static x:number = 1;
}

console.log(A.x);
A.x = 2;
console.log(A.x);

上面的代码没有编译错误,所以,看起来静态成员是可以被更改的。

class A {
    static x:number = 1;
    public get_x = ():number => A.x;
}

var a = new A();
var B:typeof A = A;
console.log(a.get_x());
B.x = 2;
console.log(a.get_x());
var b = new B();
console.log(b.get_x());

上面的代码演示了一下 typeof 的用法,并且也对静态成员作了更改。

7.5. 父类引用

ts 的实例方法中,使用 super 作为对父类的引用。

class A {
    name:string;
    constructor(name:string){this.name = name;}
    func():void{
        console.log('A');
    }
}

class B extends A {
    constructor(name:string){
        super(name);
        this.name += 'XXXX';
    }
    func():void{
        super.func();
        console.log('B');
    }
}

var b = new B('B');
b.func();
console.log(b.name);

上面的代码中,特别注意一下 constructor 构造函数中 super 的处理方式,不是 super.constructor

前面的代码对应的 js 代码是:

var __extends = (this && this.__extends) || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
var A = (function () {
    function A(name) {
        this.name = name;
    }
    A.prototype.func = function () {
        console.log('A');
    };
    return A;
})();
var B = (function (_super) {
    __extends(B, _super);
    function B(name) {
        _super.call(this, name);
        this.name += 'XXXX';
    }
    B.prototype.func = function () {
        _super.prototype.func.call(this);
        console.log('B');
    };
    return B;
})(A);
var b = new B('B');
b.func();
console.log(b.name);

大概看一下,继承的实现是通过“传参”实现的,而 super 的则是通过“绑定上下文”来实现的。

提到上下文的话,我们再确定一下:

class A {
    name:string = 'A';
    func():void{
        console.log(this.name);
    }
}

class B extends A {
    name:string = 'B';
    func():void{
        super.func();
        console.log('B');
    }
}

(new B()).func();

没问题,结果是两个 B

super 的作用,只能发生在“方法”上,所以,运用“箭头函数”,把属性的值做成函数的方法,在实现中是不能使用 super 的:

class A {
    func = () => console.log('A');
}

class B extends A {
    func = () => {
        console.log('B');
    }
}

(new B()).func();

这样没问题。但是:

class B extends A {
    func = () => {
        super.func();
        console.log('B');
    }
}

这样是不行的,因为 super 只能访问到父类的方法,准确地说,是 publicprotected 的方法。

说到这个 protected ,先看 private

class A {
    private func():void{
        console.log('A');
    }
}

class B extends A {
    say():void {
        this.func();
    }
}

(new B()).say();

上面的代码是错误的, privatefunc 方法,不能被子类 B 的实例方法。

如果把 private 换成 protected 就可以了:

class A {
    protected func():void{
        console.log('A');
    }
}

class B extends A {
    say():void {
        this.func();
    }
}

(new B()).say();

当然,不直接用到了 private 的方法肯定是没问题的:

class A {
    private name:string = 'MMM';
    private func():void{
        console.log(this.name);
    }
    say():void{
        this.func();
        console.log('say');
    }
}

class B extends A {
    say():void{
        super.say();
        console.log('BB');
    }
}

(new B()).say();

子类覆写父类方法,在方法的“类型”限定上,的需要遵循一些规则。

7.6. 接口

说完了类,再来看“接口”。

interface X {
    name:string;
    set_name(name:string):A;
}

class A implements X {
    name:string = '';
    set_name(name:string){
        this.name = name;
        return this;
    }
}

接口只是规则了一套规则,或者说只是定义了一套约束,它跟生成的 js 代码完全没有关系,接口只用于 ts 的类型检查。

一个类,可以实现多个接口:

interface X {
    name:string;
    set_name(name:string):A;
}
interface Y {
    type:string;
}

class A implements X,Y {
    name:string = '';
    type:string = 'A';
    set_name(name:string){
        this.name = name;
        return this;
    }
}

接口中可以约束构造函数,其实也不能叫“约束构造函数”,我X,我也不知道应该怎么说了。

意思就是定义的接口中的“构造函数”部分,这个接口不能被用来 implements ,只能用来作为一个“类型限定”,在类型检查时用到“构造函数”约束这部分。

构造这部分不能写为 constructor , 要使用 new 代替。

见如下代码:

interface X {
    new(name:string);
}

class A implements X {
    constructor(name:string){ }
}

这样写是不行的(我也不知道为什么不行啊)。 X 中定义了 newX 不能被用来 implements

interface X {
    new(name:string):Y;
}

interface Y { }

class A implements Y {
    constructor(name:string){
        console.log(name);
    }
}

function func(cls:X){
    new cls('123');
}

func(A);

接口中的 new 是这样用的。

关于接口,还有一条机制,同名的接口定义,规则会作合并处理,不会提示名字冲突。

interface X {
    name: string;
}

interface X {
    type: string;
}

class A implements X {
    name: string = '123';
    type: string = 'hello';
}

7.7. 多态

因为 js 本身是动态语言,也没有类型限制,所以,这里说的“多态”仅指 ts 的“编译期”的类型检查相关的内容。

前面介绍接口的时候,已经有“面向接口”的例子了。

interface X {
    get_name(name:string):string;
}

function func(obj:X):void{
    console.log(obj.get_name('haha'));
}


class A implements X {
    get_name = (name:string) => name;
}

var a = new A();
func(a);

不过事实上,类型约束中的接口,只是一个“说明”性质的东西,以上面代码来说,它只是表达了“传入的参数要像接口 X 声明的那样”,至于到底是不是“实现了接口 X ”这无关重要。即:

interface X {
    get_name(name:string):string;
}

function func(obj:X):void{
    console.log(obj.get_name('haha'));
}

class A {
    get_name = (name:string) => name;
}

var a = new A();
func(a);

上面 A 没有声明实现了 X ,也是没有问题的,只要有 get_name 方法。

对于普通的类型声明,规则也跟上面接口一样:

class A {
    name:string = 'A';
    a_func(){}
}

class B{
    name:string = 'B';
}

var b:A = new B();
console.log(b.name);

上面的代码会报错,但是报错的原因,不是说 B 不是 A 的子类,而是, B 中没有 a_func 这个方法而已。

所以:

class A {
    name:string = 'A';
    a_func(){}
}

class B{
    name:string = 'B';
    b_func(){}
}

var b:A = new B();
console.log(b.name);

这样是没问题的。对于函数的参数同理:

class A {
    name:string = 'A';
    a_func(){}
}

class B {
    name:string = 'B';
    a_func(){}
}

function func(obj:A){
    console.log(obj.name);
}

func(new B());

简而言之, ts 中虽然有强类型的形式,但是,类型检查还是 “鸭子类型”那种规则。

7.8. 元类

元类,简单来说就是考虑如何动态地创建一个“类”。

TypeScript 在这方面没有补充新的东西,同时,因为使用了 new 作为实例化的方法,所以,在统一的形式上是没有办法有进一层的抽象了。

new 的作用需要的是一个函数 Function,但是 new 的结果,却一定是一个 Object 。所以,我们没有办法通过“实例化”来得到一个新的类(函数)。

当然,如果不管 new ,那自己随便怎么写都好说了。更何况, js 中还有一个无所不能的 new Function() 呢。

7.9. 反射

同样,在这方面 ts 也没有提供新的东西。 js 层面,我们能通过 for in 有限地获取成员,但是,获取父类,子类这些东西就没有标准的作法了。说到这里,想到 ts 在生成 js 的时候,在实现继承的方式上会自己定义一个 __extends 函数。但是编译器对 __extends 这个名字是没有作特殊保护的。

var __extends = function(){}
class A {
    static a:string = 'A';
}
class B extends A {}
console.log(B.a);

7.10. 泛型

关于类型前面都讲得差不多了,现在考虑,类型本身是动态的情况。

这里最简单的例子,就是“传入什么,就传出什么”。

function func(a){
    return a;
}

这是一个最简单不过的函数,但是,当我们考虑给它加上类型约束的时候,却碰到无法解决的困难了。

可能有人直观地会想到这样做:

function func(a:any):any{
    return a;
}

“所有类型都可以”嘛,显然,这是不符合我们的“传入什么,就传出什么”这个目的的,按这种约束,传入一个 number ,传出一个 string 都满足 any 类型的约束。

实际上,这里我们需要的不是“任何类型”,而是“跟传入值一样的类型”。这种动态的,确定的类型约束,就需要用到“泛型”机制。在 ts 中使用尖括号定义“泛型变量”来实现:

function func<T>(a:T):T{
    return a;
}

这里的 T 只是一个普通的引用变量名,它也完全可以是其它的写法:

function func<what>(a:what):what{
    return a;
}

定义之后,在调用时,同样也是使用尖括号来人为指定一种类型(确定的类型):

func<string>('123');
func<number>(123);
func<number[]>([11]);
func<[number, string]>([11, 'ss']);

这种方式明确在调用时给出的类型信息,这样 ts 就可以正常进行类型检查了。

箭头函数也可以使用:

var func = <T>(a:T):T => a;
func<number>(123);

甚至是类也可以使用“泛型变量”:

class A<T, U> {
    number:T;
    age:U;
}

var a = new A<number, string>();
a.number = 123;
a.age = '11';

这里注意,“泛型变量”只用于类型约束,类型约束只在编译期起作用,它跟在运行期起作用的普通变量的层次是不同的。

接口中同样可以使用“泛型变量”:

interface X<T> {
    number:T
}

class A implements X<string> {
    number: string;
}

直接用于类型约束中:

interface X<T> {
    number:T
}

function func<T>(obj:X<T>):T{
    return obj.number;
}

func<string>({number: '11'});

约束成员函数:

function func<T>(obj: { new():T; show():string }):T{
    return new obj();
}


class A {
    static show():string{return 'A'}
}

func<A>(A);

“泛型参数”也可以直接描述继承关系(implements 不可以用):

class A {
    number:string;
}

function func<T extends A>(obj):T{
    return new obj();
}

class B {}
func<B>(B); //ERROR

8. 名字空间与模块加载

8.1. 名字空间

ts 中专门添加了名字空间机制,使用 namespace 处理:

namespace A {
    export var name:string;
    name = '123';
    var age:number = 11;
}
console.log(A.name);

对应的 js 代码为:

var A;
(function (A) {
    A.name = '123';
    var age = 11;
})(A || (A = {}));
console.log(A.name);

名字空间中需要导出的变量,使用 export 标识出来。名字空间本身的嵌套也是如此:

namespace A {
    export var name:string;
    name = '123';
    var age:number = 11;

    export namespace B {
        export var name:string;
        name = 'in b';
    }
}
console.log(A.name);
console.log(A.B.name);

生成的 js 代码为:

var A;
(function (A) {
    A.name = '123';
    var age = 11;
    var B;
    (function (B) {
        B.name = 'in b';
    })(B = A.B || (A.B = {}));
})(A || (A = {}));
console.log(A.name);
console.log(A.B.name);

名字空间和接口一样,同名的会自动合并,即使是在不同文件中也没有关系。

namespace A {
    export var name:string = 'A';
}

namespace A {
    export var age:number = 123;
}
console.log(A.name);
console.log(A.age);

生成的 js 代码是:

var A;
(function (A) {
    A.name = 'A';
})(A || (A = {}));
var A;
(function (A) {
    A.age = 123;
})(A || (A = {}));
console.log(A.name);
console.log(A.age);

8.2. 生成js模块

js 的模块机制,市面上大概分成 CommonJS 和 AMD,及一个想通吃的 UMD 。 typescript 在转换到 js 时,可以根据配置对应这几种模块机制。

对于 ts 来说,只要在一个文件的顶级空间中有 export 或者 import ,那么 ts 就认为此文件是在定义一个“模块”,在作编译时,就会把 ts 代码转换成相应的 js 模块。

以 AMD 为例,我们写代码( hello.ts ):

export var a:string;
import b = require('./other');
a = '131';
console.log(b.name);

要作编译时通过参数指定模块加载使用的类型:

tsc --module amd hello.ts

对应生成的 js 就是:

define(["require", "exports", './other'], function (require, exports, b) {
    exports.a = '131';
    console.log(b.name);
});

ts 的 AMD 方式是以 requirejs 为准的。

一个文件如果被 ts 认为是一个模块了的话,那么最后生成的 js 一定只是此文件的内容。相反,如果一个 ts 文件,里面没有关系模块的内容,那么在编译时是可以通过参数把所有的 ts 文件拼在一起,只生成一个 js 文件的。

所以,如果我们是在前端环境中使用 ts 的话,那就把 ts 看成是 js 文件的单纯的预处理,一个 ts 文件对应一个 js 文件,考虑这个 ts 文件生成的 js 模块文件是什么样子就好了。

8.3. tsconfig.json

tsconfig.json 是在项目的根目录下,作为编译行为的配置文件。在里面可以设置“模块类型”,“是否压缩”,“忽略哪些目录”等选项。

大概的样子是:

{
    "compilerOptions": {
        "module": "commonjs",
        "noImplicitAny": true,
        "removeComments": true,
        "preserveConstEnums": true,
        "out": "../../built/local/tsc.js",
        "sourceMap": true
    },
    "exclude": [
        "node_modules",
        "wwwroot"
    ]
}

详情参见: https://github.com/Microsoft/typescript/wiki/tsconfig.json

因为 tsconfig.json 包含了 tsc 需要的信息,所以,存在 tsconfig.json 的情况下,简单地输入 tsc 不需要任何参数就可以完成项目编译了。

9. 开发调试

ts 的强类型机制配合 IDE 的对应功能可以在开发期提供强大的检查能力,如果是作浏览器端的应用,可以通过编译把每个 ts 文件作成 AMD 标准的模块。之后通过 requirejs 加载即可。

TypeScript 转到 AMD 标准的 Javascript 之时, tsc 默认还会生成一个对应的 map 文件。这样,即使最终你在 html 文件中引用的是编译之后的 js 文件,但是因为 map 文件的存在,在 Chrome 中你是可以直接调试到原始 ts 文件的(就当 js 文件不存在一样),甚至是在 ts 文件上打断点。

PS:才看到, RequireJS 这个项目现在是属于 Dojo 基金会的了。

10. 总结

MS 以一已之力硬是给最开始是纯脚本定位的 javascript 语言加上了强类型,以此赋于其准确的静态检查能力。这一点是让众动态语言很眼馋的东西啊。

强类型和大众化的OO机制,显然不是为了提升开发效率的,甚至还会因为更多的约束而降低效率。但是,强类型和大众化的OO机制,却一定是为协作及质量控制大大降低了门槛。以 TypeScript 组织的代码虽然不一定行数会少,但是我觉得它为前端项目的持续维护提供了一种新的可能。

TypeScript 在其开发中,似乎有一个原则,就是生成的 js 代码要是人为可读的,并且完全兼容 JavaScripttsjs 的超集)。为此,它放弃了很多可以对旧版本的 ECMA Script 作转换的新语法功能。就是说一个语法功能如果不能简单转换到旧版本的 ECMA Script 上,比如 Generator ,那么 TypeScript 就不会实现它。但是同时 TypeScriptECMA Script 新版本之间又是互相促进的关系,所以如果一个语法功能进了 ECMA Script 的新标准,那么 TypeScript 可能会在目标为新版本的 ECMA Script 中选择支持它。

对于这一点,我的感觉就是 —— 遗憾。在新版本的 ECMA Script 无法普及的情况下(即使是新版本,我也不认为它的语法足够“先进”与“严谨”,折腾那么长时间结果出来的东西还当不了 Python 2.x 的水平,更不用说 Ruby ),还要考虑生成的 js 问题, TypeScript 能做的事,除了类型之外,本身就不多了。那套粗糙原始的OO机制是 TypeScript 的最大语法贡献,也是 JavaScript 语言本身的极限了吧。

如果不是受限于 JavaScript ,我相信 TypeScript 绝对可以做得更好的。而且,目标语言也不一定仅限于 JavaScript 吧。

最后,我觉得 TypeScript 本身带来了一个启示的意义,一种确实可行的方式,为动态语言作语法扩展,使其具备更佳的静态分析的可能。说不定什么时候会出现 Tython 之类的东西呢。

评论
©2010-2022 zouyesheng.com All rights reserved. Powered by GitHub , txt2tags , MathJax