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 仍是得多看文檔,多實踐。

相關文章
相關標籤/搜索