JavaScript 在處理程式碼模組化方面有著悠久的歷史。TypeScript 自 2012 年問世以來,已經實現了對其中許多格式的支援,但隨著時間的推移,社群和 JavaScript 規範已收斂於一種稱為 ES Modules(或 ES6 模組)的格式。你可能透過 import/export 語法瞭解它。
ES Modules 於 2015 年被新增到 JavaScript 規範中,到 2020 年,大多數 Web 瀏覽器和 JavaScript 執行時都已經廣泛支援它。
為了集中精力,本手冊將涵蓋 ES Modules 及其流行的前身 CommonJS 的 module.exports = 語法。你可以在參考部分的模組 (Modules)頁面中找到關於其他模組模式的資訊。
JavaScript 模組的定義方式
在 TypeScript 中,正如在 ECMAScript 2015 中一樣,任何包含頂級 import 或 export 的檔案都被視為模組。
相反,沒有頂級 import 或 export 宣告的檔案會被視為指令碼,其內容在全域性作用域內可用(因此模組也可以訪問)。
模組在它們自己的作用域內執行,而不是在全域性作用域內。這意味著除非顯式地使用某種匯出形式將變數、函式、類等匯出,否則它們在模組外部是不可見的。同樣,要使用從其他模組匯出的變數、函式、類、介面等,必須使用某種匯入形式將其匯入。
非模組
在開始之前,理解 TypeScript 如何定義一個模組非常重要。JavaScript 規範宣告,任何沒有 import 宣告、export 或頂級 await 的 JavaScript 檔案都應被視為指令碼,而不是模組。
在指令碼檔案中,變數和型別被宣告在共享的全域性作用域中,並且通常假設你將使用 outFile 編譯器選項將多個輸入檔案合併為一個輸出檔案,或者在 HTML 中使用多個 <script> 標籤來載入這些檔案(且順序必須正確!)。
如果你有一個當前沒有任何 import 或 export 的檔案,但你想讓它被視為一個模組,請新增一行:
tsTryexport {};
這會將該檔案更改為一個不匯出任何內容的模組。此語法適用於任何模組目標。
TypeScript 中的模組
在 TypeScript 中編寫基於模組的程式碼時,有三個主要方面需要考慮:
- 語法:我想要使用什麼語法來匯入和匯出內容?
- 模組解析:模組名稱(或路徑)與磁碟上的檔案之間是什麼關係?
- 模組輸出目標:編譯出的 JavaScript 模組應該是什麼樣子?
ES 模組語法
檔案可以透過 export default 宣告一個主匯出。
tsTry// @filename: hello.tsexport default functionhelloWorld () {console .log ("Hello, world!");}
然後透過以下方式匯入:
tsTryimporthelloWorld from "./hello.js";helloWorld ();
除了預設匯出之外,還可以透過省略 default 關鍵字,使用 export 匯出多個變數和函式。
tsTry// @filename: maths.tsexport varpi = 3.14;export letsquareTwo = 1.41;export constphi = 1.61;export classRandomNumberGenerator {}export functionabsolute (num : number) {if (num < 0) returnnum * -1;returnnum ;}
這些可以在另一個檔案中使用 import 語法來使用:
tsTryimport {pi ,phi ,absolute } from "./maths.js";console .log (pi );constabsPhi =absolute (phi );
其他匯入語法
匯入內容可以使用 import {old as new} 格式進行重新命名。
tsTryimport {pi asπ } from "./maths.js";console .log (π );
你可以將上述語法混合在一起使用到單個 import 中。
tsTry// @filename: maths.tsexport constpi = 3.14;export default classRandomNumberGenerator {}// @filename: app.tsimportRandomNumberGenerator , {pi asπ } from "./maths.js";RandomNumberGenerator ;console .log (π );
你可以使用 * as name 將所有匯出的物件放入一個名稱空間中。
tsTry// @filename: app.tsimport * asmath from "./maths.js";console .log (math .pi );constpositivePhi =math .absolute (math .phi );
你可以匯入一個檔案,並透過 import "./file" 方式在當前模組中不包含任何變數。
tsTry// @filename: app.tsimport "./maths.js";console .log ("3.14");
在這種情況下,import 什麼都不做。然而,maths.ts 中的所有程式碼都會被執行,這可能會觸發影響其他物件的副作用。
TypeScript 特有的 ES 模組語法
型別可以使用與 JavaScript 值相同的語法進行匯出和匯入。
tsTry// @filename: animal.tsexport typeCat = {breed : string;yearOfBirth : number };export interfaceDog {breeds : string[];yearOfBirth : number;}// @filename: app.tsimport {Cat ,Dog } from "./animal.js";typeAnimals =Cat |Dog ;
TypeScript 擴充套件了 import 語法,增加了兩個用於宣告型別匯入的概念。
import type
這是一種僅能用於匯入型別的匯入語句。
tsTry// @filename: animal.tsexport typeCat = {breed : string;yearOfBirth : number };export typeDog = {breeds : string[];yearOfBirth : number };export constcreateCatName = () => "fluffy";// @filename: valid.tsimport type {Cat ,Dog } from "./animal.js";export typeAnimals =Cat |Dog ;// @filename: app.tsimport type {createCatName } from "./animal.js";const'createCatName' cannot be used as a value because it was imported using 'import type'.1361'createCatName' cannot be used as a value because it was imported using 'import type'.name =(); createCatName
內聯 type 匯入
TypeScript 4.5 還允許在個別匯入前新增 type 字首,以指明匯入的引用是一個型別。
tsTry// @filename: app.tsimport {createCatName , typeCat , typeDog } from "./animal.js";export typeAnimals =Cat |Dog ;constname =createCatName ();
總之,這些功能允許 Babel、swc 或 esbuild 等非 TypeScript 轉譯器知道哪些匯入是可以安全刪除的。
具有 CommonJS 行為的 ES 模組語法
TypeScript 具有與 CommonJS 和 AMD 的 require 直接對應的 ES 模組語法。在大多數情況下,使用 ES 模組的匯入與這些環境中的 require 相同,但這種語法確保了你的 TypeScript 檔案與 CommonJS 輸出之間的一一對應關係。
tsTryimportfs = require("fs");constcode =fs .readFileSync ("hello.ts", "utf8");
你可以在 模組參考頁面 中瞭解更多關於此語法的資訊。
CommonJS 語法
CommonJS 是 npm 上大多數模組所採用的格式。即使你使用的是上述 ES 模組語法編寫程式碼,簡要了解 CommonJS 語法的工作方式也能幫助你更輕鬆地進行除錯。
匯出
識別符號是透過設定一個名為 module 的全域性物件上的 exports 屬性來匯出的。
tsTryfunctionabsolute (num : number) {if (num < 0) returnnum * -1;returnnum ;}module .exports = {pi : 3.14,squareTwo : 1.41,phi : 1.61,absolute ,};
然後這些檔案可以透過 require 語句匯入:
tsTryconstmaths =require ("./maths");maths .pi ;
或者你可以使用 JavaScript 中的解構特性來簡化:
tsTryconst {squareTwo } =require ("./maths");squareTwo ;
CommonJS 與 ES 模組的互操作性
在預設匯入和模組名稱空間物件匯入之間的區別方面,CommonJS 和 ES 模組的功能存在差異。TypeScript 提供了一個編譯器標誌來減少這兩套不同約束之間的摩擦,即 esModuleInterop。
TypeScript 的模組解析選項
模組解析是將 import 或 require 語句中的字串轉換為該字串所指向的檔案的過程。
TypeScript 包含兩種解析策略:Classic 和 Node。Classic 是當編譯器選項 module 不為 commonjs 時的預設策略,旨在保持向後相容性。Node 策略複製了 Node.js 在 CommonJS 模式下的工作方式,並增加了對 .ts 和 .d.ts 的檢查。
有許多 TSConfig 標誌會影響 TypeScript 中的模組策略:moduleResolution, baseUrl, paths, rootDirs。
有關這些策略如何工作的全部詳細資訊,你可以查閱 模組解析 參考頁面。
TypeScript 的模組輸出選項
有兩個選項會影響輸出的 JavaScript:
你使用哪個 target 取決於你期望執行 TypeScript 程式碼的 JavaScript 執行時中可用的特性。這可能是:你支援的最舊的 Web 瀏覽器、你預計執行的最低 Node.js 版本,或者可能來自你的執行時環境的獨特約束(例如 Electron)。
模組之間的所有通訊都透過模組載入器進行,編譯器選項 module 決定了使用哪一個。在執行時,模組載入器負責在執行模組之前定位並執行其所有依賴項。
例如,這是一個使用 ES 模組語法的 TypeScript 檔案,展示了 module 的幾個不同選項:
tsTryimport {valueOfPi } from "./constants.js";export consttwoPi =valueOfPi * 2;
ES2020
tsTryimport { valueOfPi } from "./constants.js";export const twoPi = valueOfPi * 2;
CommonJS
tsTry"use strict";Object.defineProperty(exports, "__esModule", { value: true });exports.twoPi = void 0;const constants_js_1 = require("./constants.js");exports.twoPi = constants_js_1.valueOfPi * 2;
UMD
tsTry(function (factory) {if (typeof module === "object" && typeof module.exports === "object") {var v = factory(require, exports);if (v !== undefined) module.exports = v;}else if (typeof define === "function" && define.amd) {define(["require", "exports", "./constants.js"], factory);}})(function (require, exports) {"use strict";Object.defineProperty(exports, "__esModule", { value: true });exports.twoPi = void 0;const constants_js_1 = require("./constants.js");exports.twoPi = constants_js_1.valueOfPi * 2;});
注意,ES2020 實際上與原始的
index.ts相同。
你可以在 TSConfig module 參考中檢視所有可用選項及其生成的 JavaScript 程式碼。
TypeScript 名稱空間
TypeScript 有自己的模組格式,稱為 namespaces(名稱空間),它早於 ES 模組標準。這種語法在建立複雜的定義檔案時有很多有用的特性,並且在 DefinitelyTyped 中仍然被積極使用。雖然沒有被棄用,但名稱空間中的大多數特性都已存在於 ES 模組中,我們建議你使用後者以符合 JavaScript 的發展方向。你可以在 名稱空間參考頁面 中瞭解更多關於名稱空間的資訊。