TypeScript是強型別的世界,透過預先宣告物件、屬性、方法、介面,在編輯階段提供Intellisense提示、Go Definition、Find All References、Rename... 等編譯式語言才有的功能,而編譯時可預先抓出參數、型別、方法錯誤,降低執行階段發現修復的高昂成本。
開始使用TypeScript一段時間後,一定會發現一項困擾: 雖然DefinitelyTyped計劃已匯集許多常用JavaScript程式庫的TypeScript定義檔,但畢竟無法涵蓋你會用到的每一個JavaScript程式庫,以我自己為例,馬上面對的問題的便是: 我找不到jQuery BlockUI的定義檔。
於是,在程式中要引用block(),blockUI()會出現紅蚯蚓無法編譯。
有一個取巧解法,只要不在全域範圍,在module、interface、class內部可以透過declare指令重新將jQuery、$定義成any型別,declare的變數並不會出現在JavaScript中,純粹讓編譯器假設該變數存在。如此jQuery變成任意型別物件,不管加上什麼屬性、呼叫任何方法都視為合法,但這得付出代價 – 與jQuery相關的Intellisense與編輯檢查從此失效,退回JavaScript時代。
我曾想到一個投機做法,declare var $就好,當需要強型別時寫jQuery("div"),需要用無定義檔方法時寫$("div")。不過身為程式魔人,一直偷雞摸狗下去也不是辦法,還是乖乖學會怎麼為jQuery Plugin寫定義檔吧!
這裡先試寫一個簡單但無聊的jQuery Plugin當成練習目標,為了涵蓋常遇到的各式情境,我刻意加入多種存取API:
$("…").fill(); 元素塗色(用預設顏色)
$("...").fill({ color: "red" }); 元素塗色(指定顏色)
$.fill(); 網頁塗色(用預設顏色)
$.fill({ color: "red" }); 網頁塗色(指定顏色)
$.fill.options.color = "blue"; 修改預設顏色
$.title(); 取得網頁標題
$.title("…"); 設定網頁標題
TypeScript程式碼如下:
/** jquery.fill options */
interface JQFillOptions {
/** fill color */
color?: string;
}
(function ($) {
var defaultOptions: JQFillOptions = {
color: "red"
};
//merge option and default option to get fill color
function getColor(options?: JQFillOptions) {
return $.extend({}, defaultOptions, options).color;
}
//fill background to document.body
$.fill = function (options?: JQFillOptions) {
$("body").css("background-color", getColor(options));
}
//global options
$.fill.options = defaultOptions;
//fill background for element
$.fn.fill = function (options?: JQFillOptions) {
returnthis.each(function () {
$(this).css("background-color", getColor(options));
});
};
//get and set document.title
$.title = function (title?: string) {
if (title)
document.title = title;
else
return document.title;
};
})(jQuery);
寫個網頁測試:
<!DOCTYPEhtml>
<htmlxmlns="http://www.w3.org/1999/xhtml">
<head>
<title>jQuery Fill Plugin</title>
<style>
div {
margin: 12px; padding: 6px; width: 100px;
color: white;
}
</style>
</head>
<body>
<div>Test</div>
<div>Test</div>
<scriptsrc="../Scripts/jquery-2.1.1.js"></script>
<script src="jquery.fill.js"></script>
<script>
$.fill.options.color = "yellow";
$.fill();
$("div").first().fill({ color: "blue" })
.end().last().fill({ color: "green" });
$.title($.title() + "$$");
</script>
</body>
</html>
測試成功!
接著我們把JavaScript抽出來改寫為TypeScript,一如預料,馬上遇到JQuery、JQueryStatic未定義fill、title的錯誤,無法編譯。
為了讓TypeScript認識我們的Plugin,我們需在jquery.fill.ts加入interface JQuery及interface JQueryStatic宣告,TypeScript會將它們合併到JQuery定義檔的同名interface中,如此JQuery會多了.fill()以支援$("…").fill()語法、JQueryStatic會增加.fill()、.title()以支援$.fill()、$.title()。不過有個地方比較麻煩,由於要同時支援$.fill()、$.fill.options兩種存取方式,需多宣告一個interface JQFillStatic,其中包含一個方法(options?: JQFillOptions)以及一個屬性options,而JQueryStatic interface的fill型別為JQFillStatic,如此才能讓$.fill()與$.fill.options都有效。宣告程式如下:
/** jquery.fill options */
interface JQFillOptions {
/** fill color */
color?: string;
}
interface JQuery {
/**
* 將元素填滿背景色
*
* @param options 顏色設定,未提供時依預設值
*/
fill(options?: JQFillOptions): JQuery;
}
interface JQFillStatic {
/**
* 將document.body填滿背景色
*
* @param options 顏色設定,未提供時依預設值
*/
(options?: JQFillOptions);
/** 顏色預設值 */
options?: JQFillOptions;
}
interface JQueryStatic {
fill?: JQFillStatic;
/**
* 取得或設定document.title
* @param title 要設定的網頁標題,未提供時傳回現有標題
*/
title(title?: string);
}
(function ($) {
///...省略...
})(jQuery);
如此,TypeScript就認得我們的Plugin囉~
在以上案例,JQuery Plugin用TypeScript開發,因此JQuery、JQueryStatic宣告直接寫入同一TypeScript檔即可。如果是第三方JavaScript,做法則比照scripts/typings/jquery/jquery.d.ts,要為該JavaScript檔寫一個同檔名的.d.ts。更進一步,還可將你寫好的定義檔貢獻到DefinitelyTyped,分享給開發社群,這才是新時代的好男兒! (遠目)
劍及履及,我的第一個TypeScript定義檔已經被Merge到DefintelyTyped,DefinitelyTyped會自動將其包成NuGet Package,所以jQuery BlockUI現在有TypeScript定義檔囉~
關於撰寫定義檔,TypeScript CodePlex上有篇教學: Writing Definition (.d.ts) Files,如果你也有心參與DefinitelyTyped的定義檔補完計劃,可以參考貢獻指南。