前陣子將一個中型網站的 JavaScript 翻寫成 TypeScript,轉換完數千行程式。身為 TypeScript 魯雞(Rookie),少不了一段步步踩雷、天天摔坑的日子,接著就漸入佳境,轉換後體驗到按 F2 立即更名、-調介面便知哪些地方要改的便利,令人感動不已。這段時間累積了一些心得,整理如下,供其他也要挑戰 JavaScript 轉 TypeScript 的朋友參考:
- 重要觀念:
左手TypeScript 只是輔助!
JavaScript 還是得學好,但不需要研究如何用 JavaScript 實踐繼承、介面、Namespace 等進階技巧,這部分 TypeScript 已提供一套容易理解與應用的做法。
但 TypeScript 終究只是輔助,充其量幫你「化繁為簡」、「理出頭緒」。在 JavaScript 不能 Find Reference、Goto Definition,不能靠編輯器 Rename 某個屬性或方法,無法在編譯時期找出打錯字,當 JavaScript 長到上千行,這些缺點會被放大,讓你置身地獄,而 TypeScript 算是種救贖,有助於減輕痛苦(別誤會,並不是從地獄直上天堂,大概是從 18 層爬到第 8 層吧!許多 JavaScript 及前端面對的難題仍然得咬牙面對)。既然 TypeScript 只是化繁為簡、理出頭緒,也要有繁可化,當有能力寫出複雜的 JavaScript,才能享受 TypeScript 的好處。
改用 TypeScript 不是打血清,無法讓 JavaScript 魯雞變美國隊長,充其量只是讓開發者有機會在 JavaScript 享受強型別語言的結構化與嚴謹,讓重構(Refactoring)不再是天方夜譚,讓複雜的JavaScript 程式毛線球變得沒那麼複雜,如此而已。但是,當你的 JavaScript 程式已經攪成很大一團,光這點改善就足以讓人落淚。(話說,改寫前的版本也能讓人落淚,只是哭的是要接手維護的那一位) - 雷中之王 - this
this 是我踩到最多的地雷!先前已陸續分享過:
* TypeScript的this陷阱
* 再談 TypeScript 的 this - 善用 module
TypeScript 提供了物件導向,但在實務上,我只有需要享受繼承的優點時使用 class,使用更多的是 module。基本上,建議將函式、參數儘量都包進 module,例如:module Blah {
export var Boo: string = "Foo";
var internalVar: string = "Jeffrey";
export function Test() {
alert("Test!");
}
function internalFunc() {
window.console && console.log("Test");
}
}
它將被編譯成var Blah;
(function (Blah) {
Blah.Boo = "Foo";
var internalVar = "Jeffrey";
function Test() {
alert("Test!");
}
Blah.Test = Test;
function internalFunc() {
window.console && console.log("Test");
}
})(Blah || (Blah = {}));
這種(function() { … })();的寫法稱之為Immediately Invoked Function Expression (IIFE)。
TypeScript 透過 IIFE 讓 internalVar 及 internalFunc 只在該 module 範圍內才能存取;而Boo 及 Test 前方加註 export,則視為 Boo 公開的屬性及方法,可透過 Blah.Boo 及 Blah.Test() 存取。如此,就不怕在多段程式出現同名變數或函式彼此覆蓋。
另外,有注意到 (Blah || ( Blah = {} )) 的巧妙寫法嗎?這個設計讓你可以在多段 JavaScript 重複宣告同一個 module 的不同片段,最後仍融合在一起,就像 C# 的 partial class 一樣方便。 - 關於第三方程式庫定義檔
在 TypeScript 的強型別中,所有介面、方法要經宣告定義方能使用。JavaScript 轉 TypeScript 時,一定會遇到第三方程式庫 API 介面未定義,無法編譯的情境。莫非要把第三方程式庫也翻寫成 TypeScript? 當然不是!有三種解法:
* 透過 declare var someObject; 將物件宣告成任意型別不做檢查,但 TypeScrpt 的強型別優勢也會因此消失
* 查詢 DefinitelyTyped是否有善心人士已經寫好定義檔,透過 NuGet 下載安裝
* 自己為程式庫加上定義檔,順便上傳 DefinitelyTyped 做功德。
寫定義檔不難,常用技巧就那幾個,值得花點時間學習。
延伸閱讀:為jQuery Plugin撰寫TypeScript定義檔 - 強制指定型別
在 TypeScript 中一定會遇到強型別與任意型別混用的狀況。例如我遇到的一個實例:
kendo.toString 有個多載宣告是 function toString(value: number, format: string): string;
在寫 KO Handler 時,我打算由 ko.utils.unwrapObservable(valueAccessor()) 取回數值、由 allBindingsAccessor().format 取回格式字串,要藉由 kendo.toString() 轉出格式化數字。但很不幸地,ko.utils.unwrapObservable 跟 allBindingsAccessor().format 都是任意型別(any),即便我們確定它們一定是數字跟字串,也過不了編譯器這關。面對這種狀況,可比照 C# (T) someVar 的概念,利用 <T> someVar 強迫宣告型別,該宣告並不會產生額外 JavaScript 程式碼,純粹是向編譯器拍胸脯擔保該變數的型別,這樣就不會被編譯器刁難囉!kendo.toString(
再來一個例子:
<number>ko.utils.unwrapObservable(valueAccessor()),
<string>allBindingsAccessor().format)
即便 y 是 any 型別,由於三元運算子嚴格限制冒號前後的型別必須相同,故上述寫法在 JavaScript 絕對可行,在 TypeScript 卻無法編譯,這也要靠指定型別 打通關:
var y = (x == 1) ? <any>{ a: x } : x; 搞定收工。 - HTML元素強型別轉換
前一點提到的強型別問題也常發生在 HTML 元素操作上,例如:
jQuery 集合物件的陣列元素被定義成 HTMLElement,而 checked 是 <input> 才有的屬性,需要加個轉型,改成:
(<HTMLInputElement>$(this).find(":checkbox")[0]).checked = true;
才能成功編譯成功。 - 使用列舉
在 C# 中列舉可以嚴格限制變數值範圍,杜絕打錯字的風險,也方便更名調整,好處多多,在 TypeScript 也建議多多利用。但應用時一定會面臨列舉、字串、數字間的轉換,可參考先前文章:TypeScript列舉型別 - 動態加入屬性及方法
在 JavaScript 裡,我們可以直接用 window.boo = "foo"、localStorage.foo = "boo" 為現有物件動態加上自訂屬性、方法,但它們未出現在定義檔,將導致編譯失敗。為臨時性的動態成員更動定義檔不符效益,可改寫為 window["boo"]、localStorage["boo"] 解決問題。 - 一個曲折的型別調整案例
以下的寫法還常見的,可以用來計時,其中 diff 傳回的結果即為時間長度(單位為ms):var st = new Date();
var ed = new Date();
var diff = ed – st; //耗時(ms)
但以上程式無法通過 TypeScript 編譯,理由是 Date 型別不能相減。那麼 <number>ed – <number>st 呢?很抱歉,Date 不能轉型成 number:Cannot convert 'Date' to 'number': Type 'Number' is missing property 'toDateString' from type 'Date'.
最後我的解法是把 st 及 ed 宣告成任意型別:(any 無敵!但不符合強型別精神,請參考更新說明)var st: any = new Date();
var ed: any = new Date();
var diff = ed - st;
[2014-09-19更新]
陸續接到好幾位朋友的回饋,此案例應改用 Date.getTime() 以符合 TypeScript 強型別精神,在此補上:var st: number = new Date().getTime();
var ed: number = new Date().getTime();
var diff = ed - st;
- 未賦與初值的類別屬性,第一次存取才會出現
考慮以下程式:class boo {
prop1: number;
prop2: number;
}
var b = new boo();
for (var p in b) {
alert(b + "->" + b[p]);
}
執行時會看到什麼?"prop1 –> undefined" 跟 "prop2 –> undefined"?錯!什麼都沒有。
一開始我沿襲 C# 的類別概念,總想成在類別宣告過的屬性,物件建構完時屬性就會存在,只是未被賦與初值。其實不然,在 TypeScript 類別宣告屬性但未給初值,僅是賦與該屬性的合法使用權,並不會產生任何對應的 JavaScript 程式。上述程式轉成的 JavaScript 如下:var boo = (function () {
function boo() {
}
return boo;
})();
var b = new boo();
for (var p in b) {
alert(p + "->" + b[p]);
}
如果你想確保建構物件後屬性就存在,記得要給初值:class boo {
prop1: number = 0;
prop2: number = 0;
}
var b = new boo();
for (var p in b) {
alert(p + "->" + b[p]);
}
修改後產生 JavaScript 如下,就能如預期執行了:var boo = (function () {
function boo() {
this.prop1 = 0;
this.prop2 = 0;
}
return boo;
})();
var b = new boo();
for (var p in b) {
alert(p + "->" + b[p]);
}
以上是我的新手村心得,祝大家順利升級,早日出村冒險。