TypeScript 是微软开发一门 JavaScript 超集,由大名鼎鼎的 Anders Hejlsberg 主导开发,全面兼容 JavaScript 并对它进行扩展。正如其名,TypeScript 给 JavaScript 这个动态语言加上了类型保障,使得编译期可以确定类型。目前,Angular 2 便是使用 TypeScript 开发的,而我们去看 React 的代码,也会发现里面有一部分测试代码使用的是 TS。此外,国内比较知名的项目白鹭引擎也是一开始就使用 TS 进行开发。

在编译期确定类型这样做有什么好处呢?首先对我们来说,代码提示功能可以实现;其次,有了类型的保障,很多错误可以在编译期就被发现;因为有了类型,当我们引用外部的库时,接口中限制的参数类型能够让我们更清楚接口需要如何调用,提升开发的效率。当代码的复杂度逐渐升高时,往往会发现有类型保障的语言往往更适于开发大规模的应用。而这也间接地说明 JavaScript 的开发复杂化及大规模化大势所趋。

对于传统的前端页面开发来说,也许这并没有什么,毕竟代码量也就一个小文件,几百行而已,大部分 jQuery 的 DOM 操作而已。但对于现代的开发者来说,大规模的单页面 Web 应用已经是趋势,而且 JavaScript 也从浏览器端扩展到了服务端,此时,JavaScript 的开发需要考虑的复杂度就会大很多,而有了类型保证的 TypeScript 显然可以在一定程度上在不影响已有代码的前提下,提升开发的体验。

这两天在 Visual Studio Code 上玩 TypeScript,感觉下来,确实编程体验比较好。这里就对它作一个粗浅的介绍,主要包括这个语言的基本内容简介、在 Node 及浏览器端的应用、如何结合 webpack 进行打包开发。

简介

当使用 npm install -g typescript 安装好 TypeScript 以后,我们就可以使用 tsc 这个工具编译 TS 源文件并转化成可正常使用的 js 文件了。具体关于 tsc 的使用,可以参考对应的文档

先通过一个简单的代码片断来看看 TypeScript 中如何给变量,函数添加类型吧!

1
2
3
4
5
6
7
8
9
10
// value 为 number 类型的变量
var value : number;
value = 10; // 编译通过
value = 'xxx'; // 编译报错
function sayHi(name: string): boolean {
// 必须返回布尔类型
return true;
}
sayHi(value); // 编译报错
sayHi('xxx'); // 编译通过

以上,便是一个最简单的 TypeScript 代码。在 TypeScript 类型中,我们可以通过指定为 any 来表明这个变量可以是任意类型,而平常的 JavaScript 代码中,所有变量正是任意类型。除去我们熟知的几种类型,TypeScript 中还包括几个比较特殊的类型:any, null, undefined, void,其中 void 用于函数返回时表明该函数并没有返回值。

接口与类

接口这个概念在 JavaScript 中并不存在,但在其它的很多编程语言(Java、Go、C# 等)中,都会有这个概念,它定义了一种类型,所有实现其声明的变量都可以是符合这个接口定义的。

1
2
3
4
5
6
7
8
9
10
11
12
interface Name {
firstName: string,
lastName: string
}
var name: Name;
name = {
lastName,
firstName
}
interface Shape {
intersect(ray Ray): boolean
}

另外,TS 支持泛型,如果我们打开 TS 内置的 lib.d.ts,就会发现数组声明的代码里面大量地使用了泛型这一概念。

1
2
3
4
interface Array<T> {
pop(): T;
concat<U extends T[]>(...items: U[]): T[];
}

对于像 Java 和 C++ 这类强类型语言,泛型是其很重要的一个特性。TS 中提供了泛型的支持,说白了,对于输入我们不强制限制其类型,但与此同时我们却可以强制输出与输入同类型。就像下面这样:

1
2
3
function identity<T>(arg: T): T {
return arg;
}

对于泛型,不仅仅只是用于函数上,也可以用于类中,这里就不再赘述,可以参考 https://www.typescriptlang.org/docs/handbook/generics.html

PS:不过我不觉得泛型在 JS 也能同样地是一个重要特性,类比 C 语言中的 void,可能 TS 中的 any 就够了。

连泛型都有,TS 中的概念自然少不了,事实上,ES2015 中已经标准化了 JS 中类的定义,而 TS 显然对这是兼容的,并加上了类成员的可访问范围(protected, public, private),访问范围与 Java 有些类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface People {
lastName: string;
firstName: string;
}
class Person {
static className = 'Person';
lastName: string;
firstName: string;
constructor() {}
sayHi() {
}
}

而类与接口的关系,我们可以与 Go 中的作类比,基本上实现了接口的定义字段,我们就说这个类符合该接口。另外,我们也可以通过 class Cls implements inter 的形式显式地指定某个类实现了某个接口。

为了更进一步说明 TS 中的类型及变量概念,我们需要谈一下 TS 中的「声明空间」这个概念。在 TS 中,声明空间分为两类:变量声明空间与类型声明空间。从名称上我们就很好理解这两类的区别,在 TS 中,类型声明空间包括:

1
2
3
4
5
6
class Foo {}
type Foo = {}
interface Foo {}
const enum Foo {
RED, BLUE, GREEN
}

而变量声明空间则包括我们平时的 var, let, const 声明,变量声明空间中,赋值等操作不能引用类型声明空间中的值,即如果 Foo 是一个接口,则 let foo = Foo 会报错。

注意,如果 Foo 是一个类,则 let foo = Foo 是没有任何编译问题的,这也间接说明了这两类声明空间并不是完全正交的,类就属于这两类声明空间。

模块及命名空间

JavaScript 这个语言中,最憋足的一个设计也许就是没有在语言层面定义模块化。但是,从编程语言上来看,模块化却是一个很重要的概念。这也难怪,毕竟 JavaScript 也就花了二周就造出来,而且最初的功能也只是网页里的一个辅助脚本,很多的前端页面中,JS 代码也从来都是一个文件几百行就够了,根本不会涉及到可扩展、应用级别等,自然不会用到模块系统和类等概念了。

但随着 JavaScript 的羽翼渐丰,其应用场景也大大被拓展,模块系统显然越来越重要,TypeScript 中也定义了其模块系统。其包括全局级模块和文件级模块。

与最新 JavaScript 规范类似,TypeScript 也使用 exportimport 关键词定义导出和导入模块。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// StringValidator.ts
export interface StringValidator {
isAcceptable(s: string): boolean;
}
// ZipCodeValidator.ts
import { StringValidator } from './StringValidator'
export const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
export { ZipCodeValidator }
export { ZipCodeValidator as Validator }
export default ZipCodeValidator

可以看出,这与 ES2015 中规定的几乎一致。

需要注意的是,为了保持与 COMMONJS 规范的兼容,TypeScript 还兼容一种导入导出形式(需要配套使用):

1
2
3
4
// 导出,类似于 module.exports = ZipCodeValidator
export = ZipCodeValidator
// 导入
import zip = require('./ZipCodeValidator');

但并不是所有代码都是 TypeScript 编写的,比如 jQuery,我们的代码是依赖于它的,但它显然不符合 TS 的规范,这种情况下,我们需要怎么和 TypeScript 一起配合使用呢?

.d.ts 定义文件可以给我们提供解决方案。我们只需要提供一个类似于 C 语言头文件一样的 .d.ts 文件,就可以在我们的代码中使用这些外部的库了。像一些比较知名的库,DefinitelyTyped 里都已经有非常规范的头文件。比如 jQuery.d.ts 就可以直接这么用,其中类型和方法的验证,代码提示等都会很完善:

1
2
3
4
5
/// <reference path="jquery.d.ts" />
import $ = require('jquery');
$.get('url').then(() => {
});

应用

如何快速地将 TypeScript 运用在我们的 JavaScript 项目中呢?首先,需要先了解编译上下文(compilation context)这个概念:

编译上下文是为了告诉编译器代码的组织方式、编译选项等信息。有两种方式指定编译选项等信息,一是直接使用命令行指定到文件及编译选项,二是通过指定 tsconfig.json 文件,我们可以指定编译上下文以便 TS 编译器的解析。显然在日常的开发应用中,提供一个配置文件是更合理的一种方式。所以要了解一个项目的编译选项,我们需要了解基本的 tsconfig 配置。

compilerOptions 定义了编译选项,一个典型的 compilerOptions 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"compileOnSave": true,
"compilerOptions": {
"module": "commonjs",
"noImplicitAny": true,
"removeComments": true,
"preserveConstEnums": true,
"sourceMap": true
},
"files": [
"LettersOnlyValidator.ts",
"Test.ts",
"Validation.ts",
"ZipCodeValidator.ts",
"basic.ts",
"external.ts",
"jquery.d.ts",
"node.d.ts"
],
"exclude": [
"node_modules"
]
}

详细的编译选项可以参考 https://zhongsp.gitbooks.io/typescript-handbook/content/doc/handbook/Compiler%20Options.html

而在实际的项目中,我们可能需要与自动化的工具集成到一起,这种怎么处理呢?官网的文档 其实介绍的很详细,这里以 webpack 为例说明:

先安装对应的 loader:npm install ts-loader --save-dev

而后,在 webpack.config.js 文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module.exports = {
entry: "./src/index.tsx",
output: {
filename: "bundle.js"
},
resolve: {
// Add '.ts' and '.tsx' as a resolvable extension.
extensions: ["", ".webpack.js", ".web.js", ".ts", ".tsx", ".js"]
},
module: {
loaders: [
// all files with a '.ts' or '.tsx' extension will be handled by 'ts-loader'
{ test: /\.tsx?$/, loader: "ts-loader" }
]
}
}

参考

本文是我作为初学几天的一点笔记,想到哪就记到哪,关于 TS 更详细的细节部分,可以参考下面的链接文档。

  1. https://basarat.gitbooks.io/typescript
  2. https://www.typescriptlang.org/docs/handbook/basic-types.html