當使用擴展的JavaScript庫或者插件API的時候,將需要使用聲明文件(.d.ts)來描述庫的類型。本文內容將包括如何編寫聲明文件相關的一些高級概念,然后用一些例子來展示如何將各式各樣的概念與聲明文件的描述相匹配。
流程
寫.d.ts最好是從庫的說明文檔開始,而不是代碼。從說明文檔開始可以保證思維不受實現細節的影響,並且比閱讀JS代碼容易理解。下面的例子假設是根據說明文檔寫的,並且提供調用代碼。
命名空間
當定義接口(例如"options"對象)的時候,你可以選擇是否將這些類型放入一個模塊中。這需要主觀判斷,如果.d.ts文件使用者更多的是用這些類型定義變量或者參數,並且類型的命名不會產生沖突,則將其放在全局命名空間較好。如果該類型不能夠被直接引用,或者不能給一個唯一並且合理的命名,使用一個模塊來防止與其他類型的沖突。
回調函數
很多JavaScript庫都將函數作為參數,之后傳入調用此函數時所用到的已知參數列表。在編寫這些類型的函數簽名時,不可以將這些參數標記為可選參數。正確的方式是想想"需要提供哪些參數?"(針對使用.d.ts的開發人員),而不是"哪些參數將被用到?"(針對函數被調用的時候)。
可擴展性和聲明合並
當編寫定義文件時,需要記住TypeScript的擴展現有對象的規則。可以選擇使用匿名函數類型或者接口類型來聲明一個變量:
聲明一個匿名的類型:
declare var MyPoint: { x: number; y: number; };
聲明一個接口類型:
interface SomePoint { x: number; y: number; } declare var MyPoint: SomePoint;
從使用者角度來看,這些聲明其實是一樣的,但是SomePoint類型能夠通過接口合並擴展:
interface SomePoint { z: number; } MyPoint.z = 4; // OK
是否想讓聲明的變量可擴展是主觀判斷的。通常這也比較符合JavaScript庫的目的。
類的分解
TypeScript中,類會創建兩種單獨的類型:實例類型,定義類的實例有哪些成員類型;構造函數類型,定義類的構造函數有哪些成員類型。構造函數的類型也被稱為“靜態部分”類型,因為它包含了類的靜態成員。
雖然你可以使用關鍵字"typeof"來獲取類的靜態部分的類型,有時候使用類的分解模式來寫定義文件是很有必要的,它可以明確的分離類的實例和靜態類型。
標准模式:
class A {
static st: string;
inst: number;
constructor(m: any) {}
}
分解模式:
interface A_Static { new(m: any): A_Instance; st: string; } interface A_Instance { inst: number; } declare var A: A_Static;
兩種模式差異:
1.標准模式的類可以使用extends繼承;分解模式不可以。這可能在TypeScript以后的版本中被改善,如果可以需要允許任意使用extends表達式。
2.都允許在后面添加靜態部分的成員(通過合並聲明)。
3.分解模式允許在后面添加實例部分的成員,而標准模式的不允許。
4.當使用分解模式的時候,需要為更多的類起一個合理的名稱。
命名規則
一般來說,不需要給接口加上前綴I(如:IColor)。因為TypeScript中的接口比C#或Java里的接口具有更廣泛的意義,加I的命名規則基本上沒什么用。
案例
下面看例子吧。每個例子都已經提供了庫的簡單使用(這里需要自己對函數/對象進行腦補),然后就是定義精准類型的代碼。如果有多個良好的聲明方式,也會列出來。
選項對象(參數選項)
使用代碼:
animalFactory.create("dog"); // 未通過驗證: 如果給定options,必須提供name
animalFactory.create("giraffe", { name: "ronald" });
animalFactory.create("panda", { name: "bob", height: 400 });
animalFactory.create("cat", { height: 32 }); // 未通過驗證: 如果給定options,必須提供name
類型聲明:
module animalFactory { interface AnimalOptions { name: string; height?: number; weight?: number; } function create(name: string, animalOptions?: AnimalOptions): Animal; }
帶有屬性的函數
使用代碼:
zooKeeper.workSchedule = "morning";
zooKeeper(giraffeCage);
類型聲明:
// 注意:函數必須處於模塊之前 function zooKeeper(cage: AnimalCage); module zooKeeper { var workSchedule: string; }
可使用關鍵字new也可直接調用的方法
使用代碼:
var w = widget(32, 16); var y = new widget("sprocket"); // w和y都是widgets w.sprock(); y.sprock();
類型聲明:
interface Widget { sprock(): void; } interface WidgetFactory { new(name: string): Widget; (width: number, height: number): Widget; } declare var widget: WidgetFactory;
全局/封閉的庫
使用代碼:
// 可以這樣寫 import x = require('zoo'); x.open(); // 或者 zoo.open();
類型聲明:
module zoo { function open(): void; } declare module "zoo" { export = zoo; }
外部模塊中的單一復雜對象
// 可鏈式操作的eagles import eagle = require('./eagle'); // 直接調用 eagle('bald').fly(); // 使用關鍵字"new" var eddie = new eagle(1000); // 設置屬性 eagle.favorite = 'golden';
類型聲明:
// 注意:在這里可以使用任何名稱,但是整個文件中名稱都要相同。 declare function eagle(name: string): eagle; declare module eagle { var favorite: string; function fly(): void; } interface eagle { new(awesomeness: number): eagle; } export = eagle; // 順帶加一點,node編譯需要加--module,這個在TypeScript Modules(模塊)中有提到,也給出了可運行的案例
回調函數
使用代碼:
addLater(3, 4, (x) => console.log('x = ' + x));
類型聲明:
// 注意:"void"返回類型在這里優先 function addLater(x: number, y: number, (sum: number) => void): void;
本篇基本腦補過來的... 后續如果工作中有使用到再記錄詳細的使用,之前一些列的TypeScript使用手冊的隨筆中,代碼是經過修改成可運行的,還需要繼續努力,不斷改善自己,編寫更高質量的代碼。
