赶上时代——学习TypeScript

我在2015年的时候就已经对TypeScript有所耳闻,不过呢,当时并不以为意,当时我正在开开心心地写ES5,连ES6都没开始学习。随着我从后端转到前端,我开始对JS做了一些深入的学习,逐渐学习了ES6,ES7。在具体项目中也得到了充分的应用。不过呢,越写我越觉得我写的代码不够靠谱,因为缺少类型系统,代码的可读性不够高,阅读代码时,值的传递也全凭程序员自己推导。尤其是开发JavaScript类库的时候,如果不提供TypeScript类型文件,稍微大点的库可能就不这么好用了,你得经常查看API文档,这大大降低了开发效率。没办法,我开始了我的TypeScript学习之旅。

我的学习不基于0基础,是在ES6/7的基础上进行的扩展学习

类型

JavaScript中的基础类型有Boolean,Number,String,Null,Undefined,Symbol,Object,其中前6种为原始类型。TypeScript是JavaScript的超集,所以JavaScript有的它都有,声明方式如下:

let isDone: boolean = false;

let firstName: string = 'Jeff';
let lastName: string = 'Wang';
let name: string = `${firstName} ${lastName}`;
let u: undefined = undefined;
let n: null = null; 

可以看到和JavaScript相比,只增加了类型。

不过,复杂的数据类型,例如数组的声明还是有不同的姿势的:

let list: number[] = [1, 2, 3];
let list: Array<number> = [1, 2, 3]; 

此外TypeScript还添加很多其他的类型

例如和数组相关的元组(Tuple)类型(其实就是固定数量和类型的数组类型)

let list: [string, number] = ['1', 2]; 

指定了相应顺序的数组元素的类型

还有十分重要的枚举(Enum)类型

enum PAGE_STATUS {LOADING=1, SUCCESS, ERROR}
let state = PAGE_STATUS.ERROR; 

TypeScript编译成的JavaScript代码也很有意思

var PAGE_STATUS;
(function (PAGE_STATUS) {
   PAGE_STATUS[PAGE_STATUS["LOADING"] = 1] = "LOADING";
   PAGE_STATUS[PAGE_STATUS["SUCCESS"] = 2] = "SUCCESS";
   PAGE_STATUS[PAGE_STATUS["ERROR"] = 3] = "ERROR";
})(PAGE_STATUS || (PAGE_STATUS = {}));
var state = PAGE_STATUS.ERROR; 

此外枚举也支持字符串枚举,异构枚举(字符串和数字混合),反向映射

enum Color { Red='#ff0000', Green='#00ff00', Blue='#0000ff' }
enum E { A: 'a', B: 1 } 
enum Enum {
   A
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A" 

另外还有三个比较特殊的类型,Any,Never,Void

Any指代所有的类型,对于未知元素类型的数组十分有用

let list: any[] = ['1', 2, false]; 

另外利用这个特性,还有利于引入第三方JavaScript库,因为这个类型可以“躲避”编译器的静态分析。

// any can fool the compiler
let notSure: any = 4;
notSure.unknowMethod(); // ok

let notSure: number = 4;
notSure.unknowMethod(); // Property 'unknowMethod' does not exist on type 'number'. 

Never类型代表了永不存在值的类型(哈哈,很玄啊),比较典型的应用就是错误抛出和死循环。

Void类型则代表了没有返回值的类型,这个还是得和Never区分一下,Void是存在函数返回,但不存在值,Never是压根就不存在返回!!Void类型也不适合单独声明变量,它只能被赋值undefined和null。

交叉和联合类型

另外TypeScript还提供高级类型,交叉类型和联合类型

交叉类型类似并集,两个对象类型的的交叉类型会包含两个对象类型的所有属性

function extend<t, u>(first: T, second: U): T & U {
   let result = <t & u>{};
   for (let id in first) {
       (<any>result)[id] = (<any>first)[id];
   }
   for (let id in second) {
       if (!result.hasOwnProperty(id)) {
           (<any>result)[id] = (<any>second)[id];
       }
   }
   return result;
} 

</t,>

extend方法是使用交叉类型的典型方法,它可以将两个类型的对象合并为一个混合类型的对象

联合类型则是两种互斥的类型,例如以下方法:

function getPet (): (Fish | Bird) {
 return { swim: function () {} }
} 

getPet方法返回的是Fish和Bird的联合类型,这意味着这个方法只会返回Fish和Bird其中之一的类型。

类型谓词

现在我们给出getPet的完整例子:

interface Fish {
 swim(): void;
}

interface Bird {
 fly(): void;
}

function getPet (): (Fish | Bird) {
 return { swim: function () {} }
}

const pet = getPet();
if ((<fish>pet).swim) {
 (<fish>pet).swim();
} else {
 (<bird>pet).fly();
} 

我们发现基于pet的判断我们需要添加类型推断,我们可以通过类型保护来优化这段代码

function isFish(pet: Fish | Bird ): pet is Fish {
 return (<fish>pet).swim !== undefined;
}
if (isFish(pet)) {
 pet.swim();
} else {
 pet.fly();
} 

这里的isFish使用了类型谓词 pet is Fish ,这里通过类型谓词,在 isFish(pet) 判断后,TypeScript就知道了当前分支pet可以使用swim,而else分支则是fly。这个类型谓词在类型判断中中十分有用:

type LodashIsNull = (value: any) => value is null;
type LodashIsNumber = (value: any) => value is number; 

上面的定义摘自lodash类型别名定义,充分用到了类型谓词。

类型别名

我们还可以通过类型别名自定义类型。例如我们之前getPet这个例子,我们可以自定义类型:

type FishAndBird = (Fish | Bird);

function getPet (): FishAndBird {
 return { swim: function () {} }
} 

我们可以通过类型别名做到类型的复用。

接口

之前我已经说过,学习TypeScript是为了使得代码质量更高,可读性更强以及规避一些不必要的错误。类型检查是TypeScript规避错误的核心方法之一,而接口是类型检查的重要技术手段,接口为我们的代码定义了契约,使得我们的数据能够按照契约传递,赋值,保证我们代码的健壮性。

TypeScript中接口的声明十分简洁

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

而其使用方法如下

function getDistanceBetweenTwoPoint ( startPoint: Point, endPoint: Point ): number {
 const x2 = Math.pow(startPoint.x - endPoint.x, 2);
 const y2 = Math.pow(startPoint.y - endPoint.y, 2);
 return Math.sqrt(x2 + y2);
} 

为了保证方法被正确调用,Point的规约其实就起了很大的作用

getDistanceBetweenTwoPoint({ x: '1', y: '4' }, { x: '2', y: '4' });
// Types of property 'x' are incompatible.
// Type 'string' is not assignable to type 'number'.

getDistanceBetweenTwoPoint({ x: 1, y: 4 }, { x: 2, y: 4 }); 

上面的调用方式,在TypeScript编译的时候就会提示错误,这让我们规避了一些显而易见的错误。

光光是上述接口定义可能还不能满足我们日常开发需求,例如Point的x和y实际上我们要求其不能在方法中更改,只能读取。这时我们需要修改接口定义。

interface Point {
   readonly x: number;
   readyonly y: number;
} 

现在一旦我们在方法内修改Point对象的属性,编译器就能检测到错误

function getDistanceBetweenTwoPoint ( startPoint: Point, endPoint: Point ): number {
 startPoint.x = 1;
 const x2 = Math.pow(startPoint.x - endPoint.x, 2);
 const y2 = Math.pow(startPoint.y - endPoint.y, 2);
 return Math.sqrt(x2 + y2);
}
// Cannot assign to 'x' because it is a constant or a read-only property. 

除了只读,我们可能还传入一些额外的参数,例如,我们可能需要Point的color属性,这个属性可能在Point对象被另一个函数调用时被使用。这里我们需要一个可选项,TypeScript中定义也很简单:

interface Point {
   readyonly x: number;
   readyonly y: number;
   readonly color?: string;
} 

我们再想象一种情况,如果我们需要传入我们事先不知道的属性,会发生什么?

getDistanceBetweenTwoPoint({ x: 1, y: 4, radius: 4 }, { x: 2, y: 4 }); 

编译器先生会果断为你报错

// Argument of type '{ x: number; y: number; radius: number; }' is not assignable to parameter of type 'Point'. 

最简单的办法是对传入数据进行类型断言。(就是告诉编译器先生,你劳您费心,这个数据就是Point对象,您不用管多余的属性)

getDistanceBetweenTwoPoint({ x: 1, y: 4, radius: 4 } as Point, { x: 2, y: 4 }); 

虽然方便,但毕竟没有对多余的属性进行声明,这会让根据接口进行编程的其他开发人员对这个额外属性没有感知。最好的方法是添加字符串索引签名。

interface Point {
   readonly x: number;
   readonly y: number;
   readonly color?: string;
   [propName: string]: any;
} 

这个样大家就知道原来有个属性是随便传的。不过呢,个人认为这种情况应该并不多见。

接口除了描述普通对象,还能描述函数和类。

接口对函数的描述主要是其传入参数和返回值

interface getDistanceBetweenTwoPoint {
   (startPoint: Point, endPoint: Point): number;
} 

接口描述类是非常常见的情况。

interface WorkerInterface {
 name: string;
 work(money: number): string;
}

class Programmer implements WorkerInterface {
 name: string;
 constructor (name: string) {
   this.name = name;
 }
 work (money: number): string {
   if (money > 50000) return 'happy';
   else return 'unhappy';
 }
} 

上述代码,我们定义了WorkerInterface接口,并且Programmer类实现了该接口,当我们实现自WorkerInterface的时候注定了我们Programmer有work方法。那如果我们需要更高层级的抽象呢,因为一个Worker首先是一个Person,TypeScript的接口之间可以继承,做法如下:

interface Person {
 name?: string;
 height?: number;
 weight?: number;
 birthday?: number;
 monther?: Person;
 father?: Person;
 eat? ();
 sleep? ();
}

interface Workman extends Person {
 work (money: number): string;
}

class Programmer implements Workman {
 name: string;
 constructor (name: string) {
   this.name = name;
 }
 work(money: number): string {
   if (money > 50000) return 'happy';
   else return 'unhappy';
 }
} 

甚至接口也可以继承类,只不过接口继承的只是类公共部分的定义,并不会继承实现。

interface Engineer extends Workman {
 design (): void;
} 

不知道大家有没有发现,我们接口定义似乎并不包含构造器部分的定义。很遗憾,构造器属于类静态部分,而TypeScript只能检测实例部分的定义,不过,其实我们还是可以对构造器进行类型校验的,这需要我们“转静为动”,下面一个来自官网的例子能够很好演示这一点:

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");
   }
}

let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32); 

这个是十分聪明的做法,我们可以通过同一个方法构造不同的对象,还能对构造器进行校验。

ES6中早已引入了class,也不是什么新鲜的语法。那么在TypeScript中有什么值得一提的内容呢?

访问修饰符三剑客private, public,protected

在Java中,我们想必对这三个修饰符已经习以为常了,但是我们越是习以为常就越难理解为什么我们需要这三个修饰符,作为一门曾经的玩具语言,JavaScript直到现在也没有这三个修饰符,JS程序员们为了实现内部方法发明很多方法:

有“掩耳盗铃”的

class Person {
 _inner () {}
} 

有使用Symbol的

let innerMethod = Symbol('innerMethod')
class Person {
 [innerMethod] () {}
} 

相比之下,语法式的声明就会优雅很多。

public是默认修饰符,这里我们不多阐述,我们主要关注private和public

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

new Animal('jack').name;
// Property 'name' is private and only accessible within class 'Animal'. 

private使得name属性只能在class内部访问,子类也不能访问该属性,如果要实现子类能够访问,可以使用protected。

class Animal {
 private name: string;
 protected type: string;
 constructor(name: string, type: string) {
   this.name = name;
   this.type = type;
 }
}

class Tigger extends Animal {
 constructor (name: string, type: string) {
   super(name, type);
 }
 getName (): string {
   return this.name; // error
 }
 getType (): string {
   return this.type; // ok
 }
} 

上面这个例子就可以充分说明private和protected区别。

除了这三个修饰符,TypeScript中还有readonly修饰符,使得属性只读,当然这样的话,属性默认就是public。

控制属性读写权限除了readonly,其实还有我们的老朋友getter和setter。TypeScript中的用法和ES6中类似。

是否为构造器中赋值困扰?

在JavaScript的class的构造器我们可能经常有这样的操作:

class Person {
 constructor (name, height, weight) {
   this.name = name;
   this.height = height;
   this.weight = weight;
 }
} 

是不是略显枯燥和无聊?TypeScript的定义更加简单,更加有意义。

class Person {
 constructor (private name: string, private height: number, private weight: number) {}
} 

抽象类

抽象类是一种高级抽象模式,我们可以和接口类比来说

从语法上来说

abstract class Gun {
 constructor (protected holder: string) {}
 abstract shot (): void;
 getType (): void {
   console.log('武器')
 }
}

interface Gun {
 shot(): void;
 getType(): void;
} 

抽象类和接口都不能进行实例化,但都可以定义“抽象方法”(抽象类需要添加abstract关键字)。抽象类可以实现具体的实现细节,但是接口不能。

那么为什么同时需要接口和抽象类?

个人理解,接口是单纯的契约,定义了抽象的方法和属性;抽象类则更多的是对类的共同点的抽象,所以它可以实现方法的具体细节。而且一般一个子类只能继承一个抽象类,一个类却可以实现多个接口,所以抽象类是对事物本质的描述,而接口则是对事物行为,属性的描述,关注点略有不同。

那么我们何时需要接口,何时需要抽象类呢?

个人认为一旦使用了抽象类并且在其中实现了相关细节,那么往往一个类就有了固定的行为模式,所以需小心使用;而接口则比较灵活。一种说法是:接口是设计的结果,抽象类则是重构的结果。

有重载吗?

和Java中的重载十分不一样,TypeScript中的重载并不能拆分方法实现,能做的是类型校验。

class Food {
 constructor (private name: string) {}
 getName (): string {
   return this.name;
 }
 smell (time: number): void;
 smell (): void;
 smell (x?): void {
   if (typeof x === 'number') console.log('存放了' + x + '小时, 闻起来怎么样?');
   else if (typeof x === 'undefined') console.log('问问什么味道');
 }
}

const food = new Food('apple');

food.smell();

food.smell(1);

food.smell('1'); 

这个和JavaScript的实现相差不大,只不过对于支持多种参数输入的方法而言,其调用时传入的参数会做类型校验。只能说实现了重载的部分功能。

函数

TypeScript能够对函数本身进行类型规定。先看个例子:

const myFunc: (x: number, y: string) => number =
       function (m: number, n: string): number { return 1 }; 

看到这个是不是有点懵?我们来做个拆解:

let myFunc: (x: number, y: string) => number;

myFunc = function (m: number, n: string): number { return 1 }; 

其实我们为myFunc变量添加了

(x: number, y: string) => number; 

来定义myFunc的类型,这个类型规定了函数参数类型和返回值类型,基于此,其实我们的代码可以做进一步了简化

const myFunc: (x: number, y: string) => number =
       function (m, n) { return 1 }; 

另外函数也可以定义重载,用法和类中我说的方法类似

function smell (time: number): void;
function smell (): void;
function smell (x?): void {
   if (typeof x === 'number') console.log('存放了' + x + '小时, 闻起来怎么样?');
   else if (typeof x === 'undefined') console.log('问问什么味道');
} 

泛型

假设这样一种情况,我们需要实现一个基础的数据结构TypeArray,可以装载操作各种类型的数据。我们可能这样实现:

class TypeArray {
 constructor (private list: any) {}
 public getElement (index: number): any {}
}
const arr = new TypeArray([1, 2, 3]) 

对数据没有任何规约,我们使用getElements方法时也不知道具体的返回值,这样定义并不能发挥类型系统的强大威力,不过我们可以使用泛型来改进:

class TypeArray<t> {
 constructor (private list: T[]) {}
 public getElement (index: number): T {
   return this.list[index];
 }
} 
const arr = new TypeArray<string>(['1', '2', '3']);
arr.getElement(0) // String 

此外和很多语言一样,泛型可以继承某个数据类型,还可以指定默认数据类型

export interface VueConstructor<v extends vue="Vue"> {} 

上述代码是Vue的TS声明文件,可以看到V泛型继承了Vue,同时默认为Vue。

模块和命名空间

TypeScript为我们文件的组织提供两种方式:模块和命名空间。

命名空间其实在前端开发早期用的十分广泛,那时候很多库选择把方法和实现放在某个命名空间下。例如jQuery,占用了 $ 这个命名空间。TypeScript也有类似概念,实现方式也十分相似,运用了立即运行表达式,我们用一个来自官方的例子来说明:

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
let strings = ["Hello", "98052", "101"];

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

// Show whether each string passed each validator
for (let s of strings) {
 for (let name in validators) {
     console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
 }
} 

这里有很多个Validator都在Validation空间下,其编译后的js文件如下:

var Validation;
(function (Validation) {
   var lettersRegexp = /^[A-Za-z]+$/;
   var numberRegexp = /^[0-9]+$/;
   var LettersOnlyValidator = /** @class */ (function () {
       function LettersOnlyValidator() {
       }
       LettersOnlyValidator.prototype.isAcceptable = function (s) {
           return lettersRegexp.test(s);
       };
       return LettersOnlyValidator;
   }());
   Validation.LettersOnlyValidator = LettersOnlyValidator;
   var ZipCodeValidator = /** @class */ (function () {
       function ZipCodeValidator() {
       }
       ZipCodeValidator.prototype.isAcceptable = function (s) {
           return s.length === 5 && numberRegexp.test(s);
       };
       return ZipCodeValidator;
   }());
   Validation.ZipCodeValidator = ZipCodeValidator;
})(Validation || (Validation = {}));
// Some samples to try
var strings = ["Hello", "98052", "101"];
// Validators to use
var validators = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();
// Show whether each string passed each validator
for (var _i = 0, strings_1 = strings; _i < strings_1.length; _i++) {
   var s = strings_1[_i];
   for (var name_1 in validators) {
       console.log("\"" + s + "\" - " + (validators[name_1].isAcceptable(s) ? "matches" : "does not match") + " " + name_1);
   }
} 

我们可以看到,Validation这个命名空间是这样实现的

(function (validation) {})(Validation || (Validation = {})) 

其实就是一个立即执行表达式😝

我们还可以使用三斜线指令进行文件拆分

/// <reference path="Validation.ts">
/// <reference path="LetterOnlyValidator.ts">
/// <reference path="ZipCodeValidator.ts">


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

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

// Show whether each string passed each validator
for (let s of strings) {
   for (let name in validators) {
       console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
   }
} 

LetterOnlyValidator.ts

/// <reference path="Validation.ts">
namespace Validation {
 const lettersRegexp = /^[A-Za-z]+$/;
 export class LettersOnlyValidator implements StringValidator {
     isAcceptable(s: string) {
         return lettersRegexp.test(s);
     }
 }
} 

ZipCodeValidator.ts

/// <reference path="Validation.ts">

namespace Validation {
 const numberRegexp = /^[0-9]+$/;

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

easy!!!

比命名空间更好的文件组织方式是模块,TypeScript的模块和ES6的模块的语法基本一样。这里不多赘述。这里稍微提一下TypeScript中的模块解析方式,一共两种,一种名为“classic”,一种名为“node”。

classic是解析十分简单,例如对于 /root/project/src/A.ts 中,语句 import X from "./moduleB" 的查找路径顺序为

/root/project/src/moduleB.ts
/root/preject/src/moduleB.d.ts 

而对于非相对路径,语句 import X from "moduleB" 的查找路径顺序为

/root/project/src/moduleB.ts
/root/project/src/moduleB.d.ts
/root/project/moduleB.ts
/root/project/moduleB.d.ts
/root/moduleB.ts
/root/moduleB.d.ts
/moduleB.ts
/moduleB.d.ts 

查找方式也是简单粗暴。

“node”解析方式则是参照NodeJS的模块的解析方式,除了添加ts,tsx,d.ts文件的解析外,其他解析规则基本相同,这里就不多赘述,可以自行阅读文档 模块解析

在TypeScript中我们还可以通过配置文件对模块解析进行配置。

  • 通过baseUrl指定模块开始解析的位置

  • 通过paths进行路径映射

  • 通过rootDirs指定虚拟目录(在需要国际化的项目中可以很好利用)

兼容多种模块

有时我们我们可能遇到这样的模块导入导出

Person.ts

class Person {}
export = Person 

Index.ts

import Person = require('./Person') 

这其实是CommonJS的导入导出方案,其实翻译成CommonJS模块就是

// Person.js
class Perons {}
module.export = Person

// Index.js
const Person = require('./Person') 

声明文件

我们不需要给TypeScript模块提供声明文件

可能我们已经发现TypeScript有个十分广泛的应用:给JavaScript类库提供声明文件(例如,Vue),TypeScript的设计声明文件的初衷其实就是为了利用基数庞大的JavaScript类库。(因为JavaScript类库没有类型信息,为了让TypeScript能够编译,我们需要一个说明文档,这个说明文档就是声明文件)

声明一般分为两种:全局声明及模块导出声明。

全局声明会污染全局空间,模块导出声明则需要通过 import 获取内部实现。

全局声明其实类似于环境声明,我举一个来自网上的例子:

假设有一个没有声明文件的JavaScript库,该库定义在全局命名空间中,而你需要在TypeScript中使用它,这时我们也许可以通过 var someLibrary: any; 来指定该JavaScript库,并进行使用。不过这样会让人觉得莫名其妙,好方法是使用 declare var someLibrary; 代表这里有个变量是特别声明的,是全局空间中已经存在的。

在具体应用中我们可能构建一个 global.d.ts 文件,将我们对于环境的定义全局定义在其中:

declare namespace someLibrary {
 const version: number;
 // ...
} 

至于模块导出声明,则需要使用 export 将接口,类,变量等导出,而在使用的文件则使用 import 引用。模块导出不仅限于单个模块,我们举个例子:

example.d.ts

declare module Animal {
   export class Dog {
     static sound:stringconstructor (k: string)public bark(): void}
}

declare module Person {} 

这里体现了多模块导出功能,如果我们只是一个模块,那我们可以去掉声明部分,直接进行 export 。在实际应用时,我们可以通过三斜线指令指定声明地址,然后导出:

/// <reference path="./exmaple.d.ts">
import { Dog } from 'Animal' 

定义类的导出说实话是不利于扩展的,例如我们在5.0版本中定义了当前Dog类,我们在6.0版本中扩展了Dog类,而我们希望声明是累加式的,class无法重复声明,这意味着我们无法进行扩展,怎么办?

在TypeScript语言对于ES5和ES6的定义是累加式,即ES5一份定义,而ES6在其基础上进行了扩展,我们观察一个String类的定义

lib.es5.d.ts

interface String {
   toString(): string;
   // ...
}

interface StringConstructor {
   new(value?: any): String;
   (value?: any): string;
   readonly prototype: String;
   fromCharCode(...codes: number[]): string;
} 

lib.es2015.core.d.ts

interface String {
   codePointAt(pos: number): number | undefined;
   // ...
}

interface StringConstructor {
   fromCodePoint(...codePoints: number[]): string
   raw(template: TemplateStringsArray, ...substitutions: any[]): string;
} 

我们发现String类被分成两个接口, String接口实际代表了类的prototype部分,而StringConstrurctor则代表了类的静态部分(构造器及静态变量),这样我们扩展起来就相当方便。基于此,我们对Dog类进行改造:

interface Dog {
   bark(): void;
}

interface DogConstructor {
   sound: string;
   new(k: string): Dog;
} 

现在我们可以自由扩展Dog类了🐶

不出所料的结尾

到此,我已经把我的魔法棒从ES6/7更新到了TypeScript,不过现在仅仅是第一步,我还需要在工程化,测试,代码风格,代码文档上面多做尝试,后面会陆续写一些相关主题的文章。