TypeScript 零基础入门

前言

2015 年底看过一篇文章《ES2015 & babel 实战:开发 npm 模块》,那时刚接触 ES6 不久,发觉新的 ES6 语法大大简化了 JavaScript 程序的表达方式,好比箭头函数、classasync/awaitProxy等新特性,今后写 JavaScript 更成了一种享受。可是在近一年半的实践中,发现多人维护一个大型项目时,除了使用 ES6 新特性更简单地实现功能以外,另外一个重要的事情是如何保证程序的健壮性和可维护性,在这点上,彻底无类型检查、表达方式极其灵活的 JavaScript 却显得有点吃力,尤为是当团队人员水平良莠不齐时更为严重。后来接触到了 TypeScript,它是 JavaScript 语言的超集,除了支持最新的 JavaScript 语言特性以外,还增长了很是有用的编译时类型检查特性,而代码又最终会编译成 JavaScript 来执行,很是适合本来使用 JavaScript 来开发的大型项目。javascript

我在通过半年多的深刻实践,总结了一些使用 TypeScript 的经验,写成了这一篇文章,但愿帮助 TypeScript 初学者更轻松地学习。html

什么是 TypeScript

TypeScript 是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集,并且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。安德斯·海尔斯伯格,C#的首席架构师,已工做于 TypeScript 的开发。2012 年十月份,微软发布了首个公开版本的 TypeScript,2013 年 6 月 19 日,在经历了一个预览版以后微软正式发布了正式版 TypeScript 0.9,向将来的 TypeScript 1.0 版迈进了很大一步。java

以上解释来源于 百度百科 TypeScript 词条node

结合微软开发的开源代码编辑器 Visual Studio Code,使用 TypeScript 开发项目具备如下优势:git

  • 可使用最新的 ES2017 语言特性
  • 很是精准的代码提示
  • 编辑代码时具备及时错误检查功能,能够避免诸如输错函数名这种明显的错误
  • 很是精准的代码重构功能
  • 很是方便的断点调试功能
  • 编辑器集成调试功能

在使用 TypeScript 编写 Node.js 项目时,因为长期使用 JavaScript 而养成随便在对象上附加各类东西的坏习惯,刚使用 TypeScript 时可能会有点不适,另外一个不可避免的问题是依赖的代码库不是使用 TypeScript 编写的,因为不能直接经过 import 引用这些模块,在 TypeScript 上使用时会形成一些困难。本文将对初学 TypeScript 时可能会关注的问题做简要的说明。es6

编写本文时最新的 TypeScript 版本为 v2.2.2,Node.js 最新 LTS 版本为 v6.10.2,本文的全部示例代码将基于该环境来运行。github

TypeScript 语言蜻蜓点水

在学习 TypeScript 前,你须要熟悉 ES6 语法,若是以前未接触过 ES6 能够参考我以前写过的文章 《ES2015 & babel 实战:开发 npm 模块》 及 ES6 语法相关的教程 《ECMAScript 6 入门》。可使用 TypeScript 官方网站提供的 Playround 工具在线查看 TypeScript 编译为 JavaScript 后的代码,对初学者了解 TypeScript 尤其有用。typescript

其实在 TypeScript 中是能够彻底使用纯 JavaScript 语法的(固然若是这样的话就达不到使用 TypeScript 的目的,可是在项目重构为 TypeScript 的初期能够实现 TypeScript 与 JavaScript 并存,逐步替换),好比咱们在 Playground 中输入如下代码:express

function hello(msg) { console.log("hello, " + msg); } hello('laolei'); 

能够看到输出的 JavaScript 代码也跟输入的如出一辙。npm

简单来理解,TypeScript 中的 Type 指的就是在 JavaScript 语法的基础上,增长了静态类型检查,而为了让 TypeScript 起到其应有的做用,在编写程序时咱们也加上必要的类型声明,好比:

function hello(msg: string): void { console.log(`hello, ${msg}`); } hello('laolei'); 

上例中声明了函数的参数msgstring类型,而返回值为void(没有返回值),能够看到编译后的代码仍是与前面例子同样,并无变化。若是咱们将函数调用部分改成hello(123),将会看到参数123下面画了红线:

ts-01

编译器报错Argument of type '123' is not assignable to parameter of type 'string'(参数123不能赋值给string类型),由于123number类型。须要注意的是,这个错误是在编译代码时发生的,可是 TypeScript 仍然会继续将代码编译为 JavaScript,能够看到编译后的代码也没有变化,**这代表 TypeScript 的类型检查是在编译期进行的,编译后的 JavaScript 代码并不会增长任何类型检查相关的代码,所以咱们并不须要担忧由此带来的性能问题。**也就是说,若是咱们的 TypeScript 项目编译成了 JavaScript 再被其余的 JavaScript 程序调用,而对方传递了不合法的数据类型,程序可能会抛出异常。

咱们能够尝试将参数部分msg: string改成msg: any,这时编译器没有给出任何错误,由于**any表示了此参数接受任意类型**。这在使用一些 JavaScript 项目时尤为有用,能够短期内下降使用 TypeScript 的难度,可是咱们应该尽可能避免这样用。

TypeScript 中的类型分为基础类型、接口、类、函数、泛型、枚举等几种:

基础类型

如下是 TypeScript 中的几种基础类型:

  • boolean为布尔值类型,如let isDone: Boolean = false
  • number为数值类型,如let decimal: number = 6;
  • string为字符串类型,如let color: string = 'blue'
  • 数组类型,如let list: number[] = [ 1, 2, 3 ]
  • 元组类型,如let x: [ string, number ] = [ "hello", 10 ]
  • 枚举类型,如enum Color { Red, Green, Blue }; let c: Color = Color.Green
  • any为任意类型,如let notSure: any = 4; notSure = "maybe a string instead"
  • void为空类型,如let unusable: void = undefined
  • nullundefined
  • never表示没有值的类型,如function error(message: string): never { throw new Error(message); }
  • 多种类型能够用|隔开,好比number | string表示能够是numberstring类型

never类型是 TypeScript 2.0 新增的,并不如前面几种类型那么经常使用,详细信息能够参考这里:TypeScript Handbook - Basic Types - Never

接口(interface)

如下是接口的几种常见形式:

// 定义具备 color 和 width 属性的对象 interface SuperConfug { color: string; width: number; } // readonly 表示只读,不能对其属性进行从新赋值 interface Point { readonly x: number; readonly y: number; } // ?表示属性是可选的, // [propName: string]: any 表示容许 obj[xxx] 这样的动态属性 interface SquareConfig { color?: string; width?: number; [propName: string]: any; } // 函数接口 interface SearchFunc { (source: string, subString: string): boolean; } 

实际上 TypeScript 的接口还有不少种的表示形式,详细信息能够参考这里:TypeScript Hankbook - Interfaces

函数

如下是几种函数接口的定义方式:

// 普通函数
function add(a: number, b: number): number {
  return a + b;
}

// 函数参数
function readFile(file: string, callback: (err: Error | null, data: Buffer) => void) {
  fs.readFile(file, callback);
}

// 经过 type 语句定义类型
type CallbackFunction = (err: Error | null, data: Buffer) => void;
function readFile(file: string, callback: CallbackFunction) {
  fs.readFile(file, callback);
}

// 经过 interface 语句来定义类型
interface CallbackFunction {
  (err: Error | null, data: Buffer): void;
}
function readFile(file: string, callback: CallbackFunction) {
  fs.readFile(file, callback);
}

以上几种定义方式有着微妙的差异,仍是须要在深刻实践 TypeScript 后才能合理地运用。详细信息能够参考这里:TypeScript Handbook - Functions

TypeScript 的类定义跟 JavaScript 的定义方法类型同样,可是增长了publicprivateprotectedreadonly等访问控制修饰符:

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}.`; } } 

详细信息能够参考这里:TypeScript Handbook - Classes

泛型

TypeScript 的泛型和接口使得具有较强的类型检查能力的同时,很好地兼顾了 JavaScript 语言的动态特性。如下是使用泛型的简单例子:

function identity<T>(arg: T): T { return arg; } const map = new Map<string, number>(); map.set('a', 123); function sleep(ms: number): Promise<number> { return new Promise<number>((resolve, reject) => { setTimeout(() => resolve(ms), ms); }); } 

TypeScript 2.0 以后增长了不少泛型相关的语法,好比K extends keyof T这种,对初学者来讲理解起来并不容易,平时可能也并不会使用到,详细信息能够参考这里:TypeScript Handbook - Generics

以上即是 TypeScript 相对于 JavaScript 增长的核心内容,若是你熟悉 ES6 的新语法,那学习 TypeScript 也并非什么难事,只要多阅读使用 TypeScript 编写的项目源码,适当地查阅语法文档便可。限于篇幅,若是想深刻学习 TypeScript ,能够经过如下连接浏览更详细的资料:

Hello World 程序

咱们先建立一个目录(好比helloworld)用于存放此程序,并执行npm init建立package.json文件:

$ mkdir helloworld
$ cd helloworld $ Nom init 

而后全局安装 tsc 命令:

$ Nom install -g typescript

如今新建文件server.ts

import * as http from 'http'; const server = http.createServer(function (req, res) { res.end('Hello, world'); }); server.listen(3000, function () { console.log('server is listening'); }); 

为了能执行此文件,咱们须要经过 tsc 命令来编译该 TypeScript 源码:

$ tsc server.ts

若是没有什么意外的话,此时控制台会打印出如下的出错信息:

server.ts(1,23): error TS2307: Cannot find module 'http'.

这表示没有找到http这个模块定义(TyprScript 编译时是经过查找模块的 typings 声明文件来判断模块是否存在的,而不是根据真实的 js 文件,下文会详细解释),可是咱们当前目录下仍是生成了一个新的文件server.js,咱们能够试着执行它:

$ node server.js

若是一切顺利,那么控制台将会打印出 server is listening 这样的信息,而且咱们在浏览器中访问 http://127.0.0.1:3000时也能看到正确的结果:Hello, world

如今再回过头来看看刚才的编译错误信息。因为这是一个 Node.js 项目,JavaScript 语言中并无定义http这个模块,因此咱们须要安装 Node.js 运行环境的声明文件:

$ npm install @types/node --save

安装完毕以后,再重复上文的编译过程,此时 tsc 再也不报错了。

大多数时候,为了方便咱们能够直接使用 ts-node 命令直接执行 TypeScript 源文件而不须要预先编译。首先执行如下命令安装 ts-node:

$ npm install -g ts-node

而后使用 ts-node 命令执行便可:

$ ts-node server.ts

tsconfig.json 配置文件

每一个 TypeScript 项目都须要一个 tsconfig.json 文件来指定相关的配置,好比告诉 TypeScript 编译器要将代码转换成 ES5 仍是 ES6 代码等。如下是我经常使用的最基本的 tsconfig.json 配置文件:

{
  "compilerOptions": { "module": "commonjs", "moduleResolution": "node", "target": "es6", "rootDir": "src", "outDir": "dist", "sourceMap": true, "noImplicitAny": true, "noImplicitReturns": true, "noImplicitThis": true } } 

其中:

  • modulemoduleResolution表示这是一个 Node.js 项目,使用 CommonJS 模块机制
  • target 指定将代码编译到 ES6,若是目标执行系统可能有 Node.js v0.x 的版本,可设置编译到 ES5
  • rootDir 和 outDir 指定源码输入目录和编译后的代码输出目录
  • sourceMa 指定编译时生成对应的 SourceMap 文件,这样在调试程序时能快速知道所对应的 TypeScript 源码位置
  • noImplicit 开头的几个选项指定一些更严格的检查

具体说明能够参考这里的文档:

使用了这个 tsconfig.json 配置文件以后,咱们的源码就须要所有放到 src 目录,不然使用 tsc 编译将会获得相似这样的报错信息:

error TS6059: File '/typescript-example/server.ts' is not under 'rootDir' '/typescript-example/src'. 'rootDir' is expected to contain all source files.

使用第三方模块

通常状况下在 TypeScript 中是不能"直接"使用 npm 上的模块的,好比咱们要使用 express 模块,先执行如下命令安装:

$ npm install express --save

而后新建文件 src/server.ts (本来的hello.ts 和 server.ts 文件记得删除):

import * as express from 'express'; const app = express(); app.get('/', function (req, res) { res.end('hello, world'); }) app.listen(3000, function () { console.log('server is listening'); }); 

而后使用如下命令执行:

$ ts-node src/server.ts

若是不出意外,咱们将会看到这样的报错信息:

src/server.ts(1,26): error TS7016: Could not find a declaration file for module 'express'.

报错的信息代表没有找到express模块的声明文件。因为 TypeScript 项目最终会编译成 JavaScript 代码执行,当咱们在 TypeScript 源码中引入这些被编译成 JavaScript 的模块时,它须要相应的声明文件(.d.ts文件)来知道该模块类型信息,这些声明文件能够经过设置tsconfig.json中的declaration: true来自动生成。而那些不是使用 TypeScript 编写的模块,也能够经过手动编写声明文件来兼容 TypeScript(下文会讲解)。

为了让广大开发者更方便地使用 npm 上众多非 TypeScript 开发的模块,TypeScript 官方创建了一个名叫 DefinitelyTyped 的仓库,任何人均可以经过 GitHub 在上面修改或者新增 npm 模块的声明文件,经多几年多的发展,这个仓库已经包含了大部分经常使用模块的声明文件,并且仍然在继续不断完善。当遇到缺乏模块声明文件的状况,开发者能够尝试经过 npm install @types/xxx 来安装模块声明文件便可。

如今咱们尝试执行如下命令安装 express 模块的声明文件:

$ npm install @types/express --save

没有意外,果真能成功安装。如今再经过 ts-node 来执行的时候,发现已经没有报错了。

若是咱们使用的第三方模块在 DefinitelyTyped 找不到对应声明文件,也能够尝试使用require()这个终极的解决方法,它会将模块解析成 any 类型,很差的地方就是没有静态类型检查了。好比:

const express = require('express'); const app = express(); app.get('/', function (req, res) { res.end('hello, world'); }) app.listen(3000, function () { console.log('server is listening'); }); 

编写 typings 声明文件

编写 .d.ts 文件仍是比较繁琐的,好比要完整地给 express 编写声明文件,首先得了解这个模块都有哪些接口,并且 JavaScript 模块广泛接口比较 灵活,同一个方法名可能接受各类各样的参数组合。因此,大多数状况下咱们只会定义咱们须要用到的接口,下文以 express 模块为例。

为了验证咱们编写的声明文件是否有效,首先执行如下命令将以前安装的声明文件所有删除:

$ rm -rf node_modules/@types

而后新建文件typings/express.d.ts(TypeScript 默认会自动从 typings 目录加载这些 .d.ts 文件):

declare module 'express' { /** 定义 express() 函数 */ function express(): express.Application; namespace express { /** 定义 Application 接口 */ interface Application { /** get 方法 */ get(path: string, handler: (req: Request, res: Response) => void): void; /** listen 方法 */ listen(port: number, callback: () => void): void; } /** 定义 Response 接口 */ interface Request { } /** 定义 Response 接口 */ interface Response { end(data: string): void; } } export = express; } 

说明:

  • 第一行的 declare module 'express' 表示定义 express 这个模块,这样在 TypeScript 中就能够直接 import 'express' 引用
  • 最后一行export = express,而且上面分别定义了一个 function express() 和 namespace express,这种写法是比较特殊的,我一时也无法解释清楚,反正多参照DefinitelyTyped 上其余模块的写法便可。这个问题归根结底是 express 模块经过 import * as express from 'express' 引入的时候,express自己又是一个函数,这种写法在早期的 Node.js 程序中是比较流行的,可是在使用 ES6 module 语法后,就显得很是别扭。

TSLint 代码规范检查

在编写 JavaScript 代码时,咱们能够经过 ESLint 来进行代码格式检查,编写 TypeScript 代码时也可使用 TSLint,二者在配置上也有些类似。对于初学者来讲,使用 TSLint 能够知道哪些程序的写法是不被推荐的,从而养成更好的 TypeScript 代码风格。

首先咱们执行如下命令安装 TSLint:

$ npm install tslint -g

而后新建 TSLint 配置文件 tslint.json

{
  "extends": [ "tslint:recommended" ] } 

这个配置文件指定了使用推荐的 TSLint 配置(tslint:recommended)。而后执行如下命令检查:

$ tslint src/**/*.ts

能够看到如下报错信息:

ERROR: src/server.ts[10, 3]: Calls to 'console.log' are not allowed.
ERROR: src/server.ts[5, 14]: non-arrow functions are forbidden
ERROR: src/server.ts[9, 18]: non-arrow functions are forbidden
ERROR: src/server.ts[1, 26]: ' should be "
ERROR: src/server.ts[5, 9]: ' should be "
ERROR: src/server.ts[6, 11]: ' should be "
ERROR: src/server.ts[10, 15]: ' should be "
ERROR: src/server.ts[5, 22]: Spaces before function parens are disallowed
ERROR: src/server.ts[9, 26]: Spaces before function parens are disallowed

从以上信息能够看出,咱们短短几行代码违反了 TSLint 默认配置这些规则:

  • 不容许使用 console.log
  • 使用箭头函数
  • 字符串使用双引号
  • 函数定义圆括号前无空格

固然这些风格我没法接受,能够经过修改配置文件 tslint.json 来关闭它:

{
  "extends": [ "tslint:recommended" ], "rules": { "no-console": [ false ], "only-arrow-functions": [ false ] } } 

以上配置容许使用 console.log 和 function,而字符串使用双引号和圆括号前的空格这两条可使用 tslint 命令来格式化。执行如下命令检查,并容许 ESLint 尝试自动格式化:

$ tslint --fix src/**/*.ts

此时将会输出 Fixed 6 error(s) in src/server.ts,而src/server.ts文件也将会被格式化成这样:

import * as express from "express"; const app = express(); app.get("/", function(req, res) { res.end("hello, world"); }); app.listen(3000, function() { console.log("server is listening"); }); 

因为 TSLint 的规则条目比较多,就不在此赘述,详细信息能够看 TSLint 的文档:https://palantir.github.io/tslint/

发布模块

相比直接使用 JavaScript 编写的 npm 模块,使用 TypeScript 编写的模块须要增长如下几个额外的工做:

  • 发布前将 TypeScript 源码编译成 JavaScript
  • 须要修改 tsconfig.json 的配置,使得编译时生成模块对应的 .d.ts 文件
  • 在 package.json 文件增长 types 属性

咱们以一个输出一个相加连个数值的add()函数做为例子,首先新建文件 src/math.ts

/** * 相加两个数值 * * @param a * @param b */ export function add(a: number, b: number): number { return a + b; } 

而后修改tsconfig.json文件,增长declaration选项:

{
  "compilerOptions": { "module": "commonjs", "moduleResolution": "node", "target": "es6", "rootDir": "src", "outDir": "dist", "sourceMap": true, "declaration": true, "noImplicitAny": true, "noImplicitReturns": true, "noImplicitThis": true } } 

再修改 package.json 文件,在 scripts 中增长 compile 和 prepublish 脚本,以及将 typings 指向对应的 .d.ts文件:

{
  "main": "dist/math.js", "typings": "dist/math.d.ts", "scripts": { "compile": "rm -rf dist && tsc", "prepublish": "npm run compile" } } 

若是执行 npm publish 发布模块,它会先执行 npm run compile 来编译 TypeScript 源码,因为咱们不能随便上传一些无用的模块到 npm 上,这里就不作实验了,能够手动执行 npm run compile 来编译。编译后,能够看到 dist 目录生成了三个文件:

dist/math.js 为编译后的 JavaScript 文件:

"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); /** * 相加两个数值 * * @param a * @param b */ function add(a, b) { return a + b; } exports.add = add; //# sourceMappingURL=math.js.map 

dist/math.d.ts 为对应的声明文件:

/** * 相加两个数值 * * @param a * @param b */ export declare function add(a: number, b: number): number; 

dist/math.js.map 为对应的 SouceMap 文件。

单元测试

要执行使用 TypeScript 编写的单元测试程序,能够有两种方法:

  • 先经过 tsc 编译成 JavaScript 代码后,再执行
  • 直接执行 .ts 源文件

我更倾向于直接执行 .ts 源文件,下文将以 mocha 为例演示。

首先执行如下命令安装所须要的模块:

$ npm install mocha @types/mocha chai @types/chai ts-node --save-dev

而后新建单元测试文件 src/test.ts

import { expect } from 'chai'; import { add } from './math'; describe('测试 math', function () { it('add()', function () { expect(add(1, 2)).to.equal(3); }); }); 

而后修改文件 package.json 在 scripts 中增长 test 脚本:

{
  "scripts": { "test": "mocha --compilers ts:ts-node/register src/test.ts" } } 

说明:经过 mocha 命令的 --compilers 选项指定了 .ts 后缀的文件使用 ts-node 的钩子函数来预编译。

而后执行如下命令测试:

$ npm test 

如无心外,能够看到如下结果:

测试 math
    ✓ add()

  1 passing (8ms)

后记

本文大概罗列了一些使用 TypeScript 广泛会遇到的问题及简单的说明,但愿能让初学者少走些弯路,若是想深刻学习 TypeScript 仍是得多看文档,多实践。

相关文章
相关标签/搜索