概括地說,你編寫宣告檔案的結構方式取決於庫的消費(使用)方式。在 JavaScript 中提供庫以供使用的方式有很多,你需要讓你的宣告檔案與之匹配。本指南涵蓋了如何識別常見的庫模式,以及如何編寫與這些模式相對應的宣告檔案。
每種主要的庫結構模式在模板章節都有對應的檔案。你可以從這些模板開始,以幫助你更快地入門。
識別庫的型別
首先,我們將回顧 TypeScript 宣告檔案可以表示的庫型別。我們將簡要展示每種庫是如何被使用的、它是如何被編寫的,並列出一些現實世界中的庫示例。
識別庫的結構是編寫其宣告檔案的第一步。我們將給出基於使用方式和程式碼內容來識別結構的提示。根據庫的文件和組織方式,其中一種可能比另一種更容易。我們建議使用你覺得更舒服的方法。
你應該尋找什麼?
在檢視你試圖為其編寫型別定義的庫時,需要問自己的問題。
-
你如何獲取該庫?
例如,你是否只能透過 npm 獲取它,還是隻能從 CDN 獲取?
-
你將如何匯入它?
它是否添加了一個全域性物件?它是否使用了
require或import/export語句?
不同型別庫的簡短示例
模組化庫
幾乎每個現代 Node.js 庫都屬於模組家族。這些型別的庫僅適用於具有模組載入器的 JS 環境。例如,express 僅適用於 Node.js,並且必須使用 CommonJS 的 require 函式載入。
ECMAScript 2015(也稱為 ES2015、ECMAScript 6 和 ES6)、CommonJS 和 RequireJS 對匯入一個模組有類似的概念。例如,在 JavaScript CommonJS (Node.js) 中,你會這樣寫
jsvar fs = require("fs");
在 TypeScript 或 ES6 中,import 關鍵字具有相同的目的
tsimport * as fs from "fs";
你通常會看到模組化庫在其文件中包含以下行之一
jsvar someLib = require("someLib");
或者
jsdefine(..., ['someLib'], function(someLib) {});
與全域性模組一樣,你可能會在UMD 模組的文件中看到這些示例,因此請務必檢視程式碼或文件。
從程式碼中識別模組庫
模組化庫通常至少包含以下內容中的一部分
- 無條件呼叫
require或define - 類似
import * as a from 'b';或export c;的宣告 - 對
exports或module.exports的賦值
它們很少有
- 對
window或global屬性的賦值
模組模板
有四個適用於模組的模板可用:module.d.ts、module-class.d.ts、module-function.d.ts 和 module-plugin.d.ts。
你應該首先閱讀 module.d.ts,以瞭解它們的工作原理。
如果你的模組可以像函式一樣被呼叫,則使用模板 module-function.d.ts
jsconst x = require("foo");// Note: calling 'x' as a functionconst y = x(42);
如果你的模組可以使用 new 進行構造,則使用模板 module-class.d.ts
jsconst x = require("bar");// Note: using 'new' operator on the imported variableconst y = new x("hello");
如果你有一個模組在匯入時會對其他模組進行更改,請使用模板 module-plugin.d.ts
jsconst jest = require("jest");require("jest-matchers-files");
全域性庫
全域性庫是指可以從全域性作用域(即不使用任何形式的 import)訪問的庫。許多庫只是暴露一個或多個全域性變數供使用。例如,如果你使用 jQuery,可以透過直接引用 $ 變數來使用它。
ts$(() => {console.log("hello!");});
你通常會在全域性庫的文件中看到關於如何在 HTML 指令碼標籤中使用該庫的指南。
html<script src="http://a.great.cdn.for/someLib.js"></script>
如今,大多數流行的全域性訪問庫實際上都是作為 UMD 庫編寫的(見下文)。UMD 庫文件與全域性庫文件很難區分。在編寫全域性宣告檔案之前,請確保該庫不是實際的 UMD 庫。
從程式碼中識別全域性庫
全域性庫程式碼通常非常簡單。一個全域性的“Hello, world”庫可能看起來像這樣
jsfunction createGreeting(s) {return "Hello, " + s;}
或者這樣
js// Webwindow.createGreeting = function (s) {return "Hello, " + s;};// Nodeglobal.createGreeting = function (s) {return "Hello, " + s;};// Potentially any runtimeglobalThis.createGreeting = function (s) {return "Hello, " + s;};
在檢視全域性庫程式碼時,你通常會看到
- 頂級的
var語句或function宣告 - 一個或多個對
window.someName的賦值 - 假設存在像
document或window這樣的 DOM 原生物件
你不會看到
- 對模組載入器(如
require或define)的檢查或使用 - 像
var fs = require("fs");這樣的 CommonJS/Node.js 風格的匯入 - 對
define(...)的呼叫 - 描述如何
require或匯入該庫的文件
全域性庫示例
由於將全域性庫轉換為 UMD 庫通常很容易,因此很少有流行的庫仍然以全域性風格編寫。但是,對於體積小且需要 DOM(或沒有依賴項)的庫,可能仍然是全域性的。
全域性庫模板
模板檔案 global.d.ts 定義了一個示例庫 myLib。請務必閱讀“防止名稱衝突”腳註。
UMD
UMD 模組是指既可以作為模組(透過匯入)使用,也可以作為全域性變數(在沒有模組載入器的環境中執行)使用的模組。許多流行的庫,如 Moment.js,都是以這種方式編寫的。例如,在 Node.js 中或使用 RequireJS 時,你會這樣寫
tsimport moment = require("moment");console.log(moment.format());
而在純瀏覽器環境中,你會這樣寫
jsconsole.log(moment.format());
識別 UMD 庫
UMD 模組會檢查是否存在模組載入器環境。這是一種很容易發現的模式,看起來像這樣
js(function (root, factory) {if (typeof define === "function" && define.amd) {define(["libName"], factory);} else if (typeof module === "object" && module.exports) {module.exports = factory(require("libName"));} else {root.returnExports = factory(root.libName);}}(this, function (b) {
如果你在庫程式碼中看到對 typeof define、typeof window 或 typeof module 的測試(尤其是在檔案頂部),那它幾乎肯定是一個 UMD 庫。
UMD 庫的文件也經常會展示一個顯示 require 的“在 Node.js 中使用”示例,以及一個展示使用 <script> 標籤載入指令碼的“在瀏覽器中使用”示例。
UMD 庫示例
大多數流行的庫現在都以 UMD 包的形式提供。示例包括 jQuery、Moment.js、lodash 等等。
模板
使用 module-plugin.d.ts 模板。
消費依賴項
你的庫可能有幾種依賴項。本節展示瞭如何將它們匯入宣告檔案。
對全域性庫的依賴
如果你的庫依賴於全域性庫,請使用 /// <reference types="..." /> 指令
ts/// <reference types="someLib" />function getThing(): someLib.thing;
對模組的依賴
如果你的庫依賴於模組,請使用 import 語句
tsimport * as moment from "moment";function getThing(): moment;
對 UMD 庫的依賴
來自全域性庫
如果你的全域性庫依賴於 UMD 模組,請使用 /// <reference types 指令
ts/// <reference types="moment" />function getThing(): moment;
來自模組或 UMD 庫
如果你的模組或 UMD 庫依賴於 UMD 庫,請使用 import 語句
tsimport * as someLib from "someLib";
切勿使用 /// <reference 指令來宣告對 UMD 庫的依賴!
腳註
防止名稱衝突
請注意,在編寫全域性宣告檔案時,可以在全域性作用域中定義許多型別。我們強烈建議不要這樣做,因為當一個專案中存在許多宣告檔案時,這會導致無法解決的名稱衝突。
要遵循的一個簡單規則是,僅宣告由庫定義的任何全域性變數所名稱空間化的型別。例如,如果庫定義了全域性值 'cats',你應該寫
tsdeclare namespace cats {interface KittySettings {}}
但不是
ts// at top-levelinterface CatsKittySettings {}
該指南還確保庫可以在不破壞宣告檔案使用者的情況下過渡到 UMD。
ES6 對模組呼叫簽名的影響
許多流行的庫(如 Express)在匯入時將自己作為可呼叫函式公開。例如,典型的 Express 用法如下
tsimport exp = require("express");var app = exp();
在相容 ES6 的模組載入器中,頂層物件(此處匯入為 exp)只能具有屬性;頂層模組物件絕不能是可呼叫的。
這裡最常見的解決方案是為可呼叫/可構造物件定義一個 default 匯出;模組載入器通常會自動檢測這種情況,並用 default 匯出替換頂層物件。如果你在 tsconfig.json 中配置了 "esModuleInterop": true,TypeScript 可以為你處理這個問題。