小編幫你踩地雷系列: TypeScript 適合我嗎

c9s
CornelTEK
Published in
12 min readMay 25, 2016

這篇文章,不是要歌頌 TypeScript,而是小編深入前線報導,告訴你該知道的事實和地雷 XD

JavaScript 的瓶頸

JavaScript 因其語言設計性質,並不適合大型應用程式開發,隨著應用程式複雜度提升,更多的元件、更多的介面、更多的函數,你很難透過既有的 JavaScript 語言性質,來協助你呼叫數以萬計的 API。

以重構來說,大型應用程式如果沒有型別資訊,較大的重構很容易造成 Side Effect,尤其是測試沒有涵蓋的部分(雖然測試涵蓋很重要,但在大型應用程式和緊迫的時程下,很難達到該有的水平)

當然,高測試涵蓋率也不是不能做到,只不過你會花上更多的時間成本、開發成本撰寫測試罷了。

以開發來說,每個人都不可能有時間可以翻遍上萬行程式碼,並記住每個函數回傳值、參數類型,因此大型應用程式的開發 — 其實是許多片段的修改累積而成,當你需要修改某一個函數時,透過其函數定義的型別,你可以很清楚地知道如何操作這個變數,而不會把不符合的函數套用在非預期的資料型態上。

當然,你也可以透過寫 JSDoc 的 Annotated Type 寫在 DocComment 裡面,但事實是,隨著重構不斷發生,有些 Annotation 會被遺漏,甚至忘記更新,而造成更多誤解和錯誤。

其實,不用說大型應用程式開發,在現實生活中,開發 JavaScript 不用幾千行就會開始混亂不堪了。

強型別語言成為趨勢

這也就是為何,微軟著手開發 TypeScript 將 Type Inference 注入到既有的 Javascript 語言、Facebook 著手開發 Hack (PHP with Types) 以及 Flow (TypeScript 類似的語言)。

型別資訊在開發期間,可以協助你及時發現錯誤,譬如你有一個 Function 只提供 Int 型別的參數,但你想把某個 API 呼叫的結果(你並不知道他回傳的型別)直接傳給這個 Function,在不知道回傳結果的正確型別之下,就很容易埋下地雷,因為,只要這個 API 修改了回傳值,就會造成你的 Function 出錯。

又或者是,你需要支援不同的 Storage,每個 Storage 必須要可以 fetch, store, delete,但不同的 Storage 在原生的 JavaScript 實作,只能在 Runtime 呼叫才有辦法知道是否每個 Storage 都有實作該有的方法。 TypeScript 可以透過 tsc -w 直接在編譯時間告訴你實作的 Storage 還缺哪些方法需要實作。

TypeScript 是什麼?

TypeScript 是由 Anders Hejlsberg (C#, TurboPascal 之父)所設計,其實概念很簡單,就是以現有的 JavaScript 為基礎,加上型別、介面等抽象定義,協助你更穩健的開發應用程式。

寫起來像什麼樣?可以參考這篇文章的範例 — The Benefits of Migrating from JavaScript to TypeScript

從 JavaScript 轉換的成本有多大?

Adding Class Property

雖然 TypeScript 網站上寫

TypeScript is a typed superset of JavaScript that compiles to plain JavaScript

但實際上,ES6 class 如果有操作 class property,沒有定義的 property 會在 TypeScript compiler 出錯,因為 class property 目前為 TypeScript 獨有的語法,ES6 目前沒有 class property 定義的語法。

譬如 MDN 上 (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) 第一個範例 Polygon ,會出現 height property is not defined.

所以,會需要增加所有類別使用的 property 到 class definition,透過 tsc 編譯出來的錯誤來修改即可,算是很簡單的步驟。一方面,也可以更清楚的定義目前類別中所使用的 property 有哪些。

Adding Interface for Config objects

使用 JavaScript 開發應用程式,非常容易遇到的一個問題就是 config object 的使用散落在各處,卻又常常忘記將 config 的文件更新,有時候又分不清楚config 到底是哪個 config,以至於 config 的參數用法以及參數的型態,常常要查看現有程式碼許久才知道如何使用。

透過 TypeScript 的介面功能,只要定義好各個 Config 使用的 Interface: 可選參數、必要參數、函數原型等等… 往後只要查看這個 Interface,就再也不怕搞混 config object 了。

以 jQuery Ajax Settings 為例:

External Libraries

此外,引入外部元件如 jQuery,由於其外部元件介面在應用程式中是尚未定義的,所以需要輔助工具如 typings 協助你安裝外部元件的介面定義。

透過:

typings install dt~jquery --save --global 

如上指令,會自動建立 typings 目錄,以及 typings/index.d.ts 這個索引檔,其內容如下:

/// <reference path=”globals/d3/index.d.ts” />
/// <reference path=”globals/jquery/index.d.ts” />
/// <reference path=”globals/mocha/index.d.ts” />

然後你在 tsconfig.json 中,需要將這個索引檔列入編譯檔案清單中,告訴 TypeScript 在哪邊載入定義檔,譬如:

{
“compilerOptions”: {
“allowSyntheticDefaultImports”: true,
“sourceMap”: true,
“allowJs”: true,
“outDir”: “build”
},
“exclude”: [“node_modules”],
“files”: [“typings/index.d.ts”, “entry.ts”]
}

目前其外部元件的介面 (DefinitelyTyped),據我所知,似乎是人工維護的,不過數量相當龐大,見: https://github.com/DefinitelyTyped/DefinitelyTyped

從 angular, jquery-*, d3-*, gulp-* 全部都有。

實際的情況? jQuery 的介面定義相當完整,但 d3 的 Event 定義尚有一些缺失,需要在 caller 端做轉型,譬如:

(<MouseEvent>d3.event).layerX

否則會出現 BaseEvent.layerX 尚未定義的錯誤。相關修正已經提交 PR ,若有需要的朋友可以上 GitHub 給予壓力 XD

如果你不想使用定義檔,也不想自己寫定義檔,你可以透過下面這行忽略他

declare var jQuery:any

當然,我會不建議這樣做,畢竟應用程式會有很多地方是和外部元件整合,缺乏這些定義檔,也會降低應用程式的穩定性。

ES6 Import

完美支援,沒有問題。

React JSX/TSX

完全支援,沒有問題。

ES6 Object.assign

Object.assign 目前在 TypeScript 1.8 有支援,記得將 “target” 改為 “es6”。

要注意,編譯出來的 JavaScript 會是 es6,所以如果你的環境需要 es5,你可能還是需要將 target 退回到 es5,TypeScript 目前不支援 downgrade transpile。解決方法有幾種:

  • Set target to es5, 使用 npm install object-assign 之類的 polyfill 取代 Object.assign。 (用法見下方)
  • 使用 babel plugin 將 es6 之 Object.assign 轉為 es5 環境可執行之程式碼。

Exporting symbol to window.*

以往 Javascript 函式庫會透過 window.jQuery 來注入全域符號,使用 TypeScript 編譯,會出現 window.jQuery property is not defined 錯誤。

使用如下解法即可解決:

window[‘jQuery’] = jQuery;

有時候不一定是 window object,有些 library 會把一些物件綁定在 DOMElement 上,此時也要用 el[‘foo’] 這樣的 syntax 來做注入。

Casting return value from getElementById

TypeScript 原生內建所有 DOM 的 Interface,然而 TypeScript 沒辦法知道 getElementById 的回傳類型為何,因此如果你在回傳值上操作特定類型才有的屬性,如 iframe.contentDocument 會出現 undefined property 錯誤。

這個時候就需要透過適當的轉型:

var f = <HTMLIFrameElement>e.getElementById(‘iframe’);
f.contentDocument // works fine

如果你是要在 element 上注入一些非定義屬性,可以透過下面寫法避開編譯錯誤:

f[‘externalProperty’] = 'some data';

Including Node Modules

目前 (TypeScript 1.8 以及 TypeScript@next (1.9),直接引入 node.js module ,先決條件是外部模組必須為 TypeScript 所寫的 index.ts 或者透過 typings 安裝對應的 Type Definition files.

引入 node module 以 object-assign 為例,幾個步驟如下:

npm install object-assign --save
typings install object-assign --save

引入模組則使用 Old TypeScript import syntax:

import assign = require('object-assign');

不過使用 TypeScript import syntax 要注意,若你的 tsconfig.target 設為 es6,則 tsconfig.json 要設定 module: “commonjs”,才可使用此語法。

否則,會因為編譯錯誤而無法使用:

error TS1202: Import assignment cannot be used when targeting ECMAScript 6 modules. Consider using ‘import * as ns from “mod”’, ‘import {a} from “mod”’, ‘import d from “mod”’, or another module format instead.

這邊之所以說是 Old TypeScript import syntax,是因為 ECMAScript 6 module importing 是被建議用來取代 import .. require 語法,但若你還是需要使用 AMD 或 CommonJS module ,則仍需要使用 import .. require 語法。

Common JS Loading

稍微注意一下,TypeScript 的 CommonJS writer 會使用無括號 .default ,這個語法會使較舊的瀏覽器出錯。 出錯的機率?應該非常低,小編是在使用 Google Closure 2012 年版本以及 IE8 IE9 才有這個問題。

解決方法也算簡單,只要把 tsconfig.target 設定為 es3 即可。XD

Module Loading

目前常見的 module bundling tool 有如 webpack 或 gulp ,都有 typescript 相關整合,而 webpack 有 ts-loader 可用。

不過 tsc (typescript compiler) 本身其實已經包含了 module loading 的功能,如果你的執行環境允許 ES6,你可能可以直接放棄 babel 這個緩慢的 ES6 transpiler ,因為 TypeScript 本身是 ES6 相容的。

如果你的執行環境只允許 ES5,那麼你可以在 tsconfig.json 中指定 target 為 “es5”,在 TypeScript 你依然可以使用 ES6-like class, interface .. 這些東西可以完全轉換到 es5,直接取代 babel 的角色,甚至連 webpack 都不用了。

tsc --module amd --outFile out.js entry.ts src/Foo.ts

這樣可以將所有檔案透過 AMD module loading 全部編譯到 out.js 一個檔案裡。 其中 “ — outFile” 只適用 amd 以及 system 這兩種 module loading。

當然你如果要用 ES5 還用到更多更新的 ES7 features,還是可以用 ts-loader 加上 babel-loader,只不過這樣的編譯速度會相當慢就是了,因為他得 pipe 兩層 XD

所以,如果可以,就還是直接用 typescript es5 或用 runtime 的 polyfill 吧。

好吧,如果你堅持要 typescript es6 -> (tsc) -> javascript es6 -> (babel) -> javascript es5

你可以參考這個 repository (ts-webpack)

小編我把所有該有的設定全部都搞定了,直接 clone 下來,做點設定調整就可以直接用了。

大量檔案名稱修改

你可能會想要找工具可以協助你大量修改副檔名 (js -> ts),可以參考我最近寫的 fsrename (https://github.com/c9s/fsrename)

go get -u -x github.com/c9s/fsrename/fsrename
fsrename -file -replace .js -with .ts src

記得備份

記得開新的分支,後悔了還可以換回來

專案範例?

可參考小編的 action-js ( https://github.com/c9s/action-js )

直接開始

你可以直接從這個 repository (ts-webpack) 開始。

小編我把所有該有的設定全部都搞定了,直接 clone 下來,做點設定調整就可以直接用了。

(持續更新中)

--

--

c9s
CornelTEK

Yo-an Lin, yet another programmer in 21 century