TypeScript 零基礎入門
2015 年末寫過一篇文章 《ES2015 & babel 實戰:開發 npm 模塊》 ,那時剛接觸 ES6 不久,發覺新的 ES6 語法大大簡化了 JavaScript 程序的表達方式,比如箭頭函數、 class 、 async/await 、 Proxy 等新特性,從此寫 JavaScript 更成了一種享受。但是在近一年半的實踐中,發現多人維護一個大型項目時,除了使用 ES6 新特性更簡單地實現功能之外,另一個重要的事情是如何保證程序的健壯性和可維護性,在這點上,完全 無類型檢查、表達方式極其靈活 的 JavaScript 卻顯得有點吃力,尤其是當團隊人員水平參差不齊時更為嚴重。后來接觸到了 TypeScript,它是 JavaScript 語言的超集,除了支持最新的 JavaScript 語言特性之外,還增加了非常有用的編譯時類型檢查特性,而代碼又最終會編譯成 JavaScript 來執行,非常適合原本使用 JavaScript 來開發的大型項目。
我在經過半年多的深入實踐,總結了一些使用 TypeScript 的經驗,寫成了這一篇文章,希望幫助 TypeScript 初學者更輕松地學習。
什么是 TypeScript
TypeScript 是一種由微軟開發的自由和開源的編程語言。它是 JavaScript 的一個超集,而且本質上向這個語言添加了可選的靜態類型和基于類的面向對象編程。安德斯·海爾斯伯格,C#的首席架構師,已工作于 TypeScript 的開發。2012 年十月份,微軟發布了首個公開版本的 TypeScript,2013 年 6 月 19 日,在經歷了一個預覽版之后微軟正式發布了正式版 TypeScript 0.9,向未來的 TypeScript 1.0 版邁進了很大一步。
以上解釋來源于 百度百科 TypeScript 詞條
結合微軟開發的開源代碼編輯器 Visual Studio Code ,使用 TypeScript 開發項目具有以下優點:
- 可以使用最新的 ES2017 語言特性
- 非常精準的代碼提示
- 編輯代碼時具有及時錯誤檢查功能,可以避免諸如輸錯函數名這種明顯的錯誤
- 非常精準的代碼重構功能
- 非常方便的斷點調試功能
- 編輯器集成調試功能
在使用 TypeScript 編寫 Node.js 項目時,由于長期使用 JavaScript 而養成隨便在對象上附加各種東西的壞習慣,剛使用 TypeScript 時可能會有點不適,另一個不可避免的問題是依賴的代碼庫不是使用 TypeScript 編寫的,由于不能直接通過 import 引用這些模塊,在 TypeScript 上使用時會造成一些困難。本文將對初學 TypeScript 時可能會關注的問題作簡要的說明。
編寫本文時最新的 TypeScript 版本為 v2.2.2 , Node.js 最新 LTS 版本為 v6.10.2 ,本文的所有示例代碼將基于該環境來運行。
TypeScript 語言走馬觀花
在學習 TypeScript 前,你需要熟悉 ES6 語法,如果之前未接觸過 ES6 可以參考我之前寫過的文章 《ES2015 & babel 實戰:開發 npm 模塊》 及 ES6 語法相關的教程 《ECMAScript 6 入門》 。可以使用 TypeScript 官方網站提供的 Playround 工具在線查看 TypeScript 編譯為 JavaScript 后的代碼,對初學者了解 TypeScript 尤為有用。
其實在 TypeScript 中是可以完全使用純 JavaScript 語法的( 當然如果這樣的話就達不到使用 TypeScript 的目的,但是在項目重構為 TypeScript 的初期可以實現 TypeScript 與 JavaScript 并存,逐步替換 ),比如我們在 Playground 中輸入以下代碼:
function hello(msg) {
console.log("hello, " + msg);
}
hello('laolei');
可以看到輸出的 JavaScript 代碼也跟輸入的一模一樣。
簡單來理解,TypeScript 中的 Type 指的就是在 JavaScript 語法的基礎上,增加了靜態類型檢查,而為了讓 TypeScript 起到其應有的作用,在編寫程序時我們也加上必要的類型聲明,比如:
function hello(msg: string): void {
console.log(`hello, ${msg}`);
}
hello('laolei');
上例中聲明了函數的參數 msg 為 string 類型,而返回值為 void (沒有返回值),可以看到編譯后的代碼還是與前面例子一樣,并沒有變化。如果我們將函數調用部分改為 hello(123) ,將會看到參數 123 下面畫了紅線:
編譯器報錯 Argument of type '123' is not assignable to parameter of type 'string' (參數 123 不能賦值給 string 類型),因為 123 是 number 類型。需要注意的是,這個錯誤是在編譯代碼時發生的,但是 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
- null 和 undefined
- never 表示沒有值的類型,如 function error(message: string): never { throw new Error(message); }
- 多種類型可以用 | 隔開,比如 number | string 表示可以是 number 或 string 類型
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 的定義方法類型一樣,但是增加了 public , private , protected , readonly 等訪問控制修飾符:
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
}
}
其中:
- module 和 moduleResolution 表示這是一個 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 還是得多看文檔,多實踐。
來自:http://morning.work/page/2017-04/how-to-write-a-port-scanner-in-nodejs-and-typescript.html