前陣子將一個中型網站的 JavaScript 翻寫成 TypeScript,轉換完數千行程式。身為 TypeScript 魯雞(Rookie),少不了一段步步踩雷、天天摔坑的日子,接著就漸入佳境,轉換後體驗到按  F2 立即更名、-調介面便知哪些地方要改的便利,令人感動不已。這段時間累積了一些心得,整理如下,供其他也要挑戰 JavaScript 轉 TypeScript 的朋友參考:

  1. 重要觀念:左手 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 程式已經攪成很大一團,光這點改善就足以讓人落淚。(話說,改寫前的版本也能讓人落淚,只是哭的是要接手維護的那一位)
  2. 雷中之王 - this
    this 是我踩到最多的地雷!先前已陸續分享過:
    * TypeScript的this陷阱
    * 再談 TypeScript 的 this
  3. 善用 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 一樣方便。

  4. 關於第三方程式庫定義檔
    在 TypeScript 的強型別中,所有介面、方法要經宣告定義方能使用。JavaScript 轉 TypeScript 時,一定會遇到第三方程式庫 API 介面未定義,無法編譯的情境。莫非要把第三方程式庫也翻寫成 TypeScript? 當然不是!有三種解法:
    * 透過 declare var someObject; 將物件宣告成任意型別不做檢查,但 TypeScrpt 的強型別優勢也會因此消失
    * 查詢 DefinitelyTyped 是否有善心人士已經寫好定義檔,透過 NuGet 下載安裝
    * 自己為程式庫加上定義檔,順便上傳 DefinitelyTyped 做功德。
    寫定義檔不難,常用技巧就那幾個,值得花點時間學習。
    延伸閱讀:為jQuery Plugin撰寫TypeScript定義檔
  5. 強制指定型別
    在 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; 搞定收工。
  6. HTML元素強型別轉換
    前一點提到的強型別問題也常發生在 HTML 元素操作上,例如: 

    jQuery 集合物件的陣列元素被定義成 HTMLElement,而 checked 是 <input> 才有的屬性,需要加個轉型,改成:
    (<HTMLInputElement>$(this).find(":checkbox")[0]).checked = true;
    才能成功編譯成功。
  7. 使用列舉
    在 C# 中列舉可以嚴格限制變數值範圍,杜絕打錯字的風險,也方便更名調整,好處多多,在 TypeScript 也建議多多利用。但應用時一定會面臨列舉、字串、數字間的轉換,可參考先前文章:TypeScript列舉型別
  8. 動態加入屬性及方法
    在 JavaScript 裡,我們可以直接用 window.boo = "foo"、localStorage.foo = "boo" 為現有物件動態加上自訂屬性、方法,但它們未出現在定義檔,將導致編譯失敗。為臨時性的動態成員更動定義檔不符效益,可改寫為 window["boo"]、localStorage["boo"] 解決問題。
  9. 一個曲折的型別調整案例
    以下的寫法還常見的,可以用來計時,其中 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;

  10. 未賦與初值的類別屬性,第一次存取才會出現
    考慮以下程式:
    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]);
    }

以上是我的新手村心得,祝大家順利升級,早日出村冒險。


Comments

# by Will

第9點建議寫法: var st: number = new Date().getTime(); var ed: number = new Date().getTime(); var diff = ed - st;

# by Zakk

好文收藏~

# by TypeScript新手

Hello , 黑大 , 關於module,想請教個小問題困擾好久: 我有個myModule.ts 如下: module myModule { export var name: string = "Hello"; } 有個main.ts當作程式的入口(使用webpack打包) import './modules/myModule' console.log(myModule.name); 但這樣ts編譯會過,但瀏覽器的console都會出錯,錯誤訊息是 myModule is not defined 不太清楚是到底是哪裡出錯了,謝謝@@

# by Jeffrey

to TypeScript新手, 要在網頁實現模組動態載入,網頁必須有模組載入程式(Module Loader,如:RequireJS): Modules import one another using a module loader. At runtime the module loader is responsible for locating and executing all dependencies of a module before executing it. Well-known modules loaders used in JavaScript are the CommonJS module loader for Node.js and require.js for Web applications. https://www.typescriptlang.org/docs/handbook/modules.html

Post a comment