TypeScript 的官方文件早已更新,但我能找到的中文文件都還停留在比較老的版本。所以對其中新增以及修訂較多的一些章節進行了翻譯整理。
本篇翻譯整理自 TypeScript Handbook 中 「Module」 章節。
本文並不嚴格按照原文翻譯,對部分內容也做了解釋補充。
模組(Module)
JavaScript 有一個很長的處理模組化程式碼的歷史,TypeScript 從 2012 年開始跟進,現在已經實現支援了很多格式。不過隨著時間流逝,社群和 JavaScript 規範已經收斂為名為 ES 模組(或者 ES6 模組)的格式,這也就是我們所知的 import/export
語法。
ES 模組在 2015 年被新增到 JavaScript 規範中,到 2020 年,大部分的 web 瀏覽器和 JavaScript 執行環境都已經廣泛支援。
本章將覆蓋講解 ES 模組和和它之前流行的前身 CommonJS module.exports =
語法,你可以在 Modules 章節找到其他的模組模式。
JavaScript 模組是如何被定義的(How JavaScript Modules are Defined)
在 TypeScript 中,就像在 ECMAScript 2015 中 ,任何包含了一個頂層 import
或者 export
的檔案會被認為是一個模組。
相對應的,一個沒有頂層匯入和匯出宣告的檔案會被認為是一個指令碼,它的內容會在全域性範圍內可用。
模組會在它自己的作用域,而不是在全域性作用域裡執行。這意味著,在一個模組中宣告的變數、函式、類等,對於模組之外的程式碼都是不可見的,除非你顯示的匯出這些值。
相對應的,要消費一個從另一個的模組匯出的值、函式、類、介面等,也需要使用匯入的格式先被匯入。
非模組(Non-modules)
在我們開始之前,我們需要先理解 TypeScript 認為什麼是一個模組。JavaScript 規範宣告任何沒有 export
或者頂層 await
的 JavaScript 檔案都應該被認為是一個指令碼,而非一個模組。
在一個指令碼檔案中,變數和型別會被宣告在共享的全域性作用域,它會被假定你或者使用 outFile 編譯選項,將多個輸入檔案合併成一個輸出檔案,或者在 HTML使用多個 <script>
標籤載入這些檔案。
如果你有一個檔案,現在沒有任何 import
或者 export
,但是你希望它被作為模組處理,新增這行程式碼:
export {};
這會把檔案改成一個沒有匯出任何內容的模組,這個語法可以生效,無論你的模組目標是什麼。
TypeScript 中的模組(Modules in TypeScript)
在 TypeScript 中,當寫一個基於模組的程式碼時,有三個主要的事情需要考慮:
- 語法:我想匯出或者匯入該用什麼語法?
- 模組解析:模組名字(或路徑)和硬碟檔案之間的關係是什麼樣的?
- 模組匯出目標:匯出的 JavaScript 模組長什麼樣?
ES 模組語法(ES Module Syntax)
一個檔案可以通過 export default
宣告一個主要的匯出:
// @filename: hello.ts
export default function helloWorld() {
console.log("Hello, world!");
}
然後用這種方式匯入:
import hello from "./hello.js";
hello();
除了預設匯出,你可以通過省略 default
的 export
語法匯出不止一個變數和函式:
// @filename: maths.ts
export var pi = 3.14;
export let squareTwo = 1.41;
export const phi = 1.61;
export class RandomNumberGenerator {}
export function absolute(num: number) {
if (num < 0) return num * -1;
return num;
}
這些可以在其他的檔案通過 import
語法引入:
import { pi, phi, absolute } from "./maths.js";
console.log(pi);
const absPhi = absolute(phi);
// const absPhi: number
附加匯入語法(Additional Import Syntax)
一個匯入也可以使用類似於 import {old as new}
的格式被重新命名:
import { pi as π } from "./maths.js";
console.log(π);
// (alias) var π: number
// import π
你可以混合使用上面的語法,寫成一個單獨的 import
:
// @filename: maths.ts
export const pi = 3.14;
export default class RandomNumberGenerator {}
// @filename: app.ts
import RNGen, { pi as π } from "./maths.js";
RNGen;
(alias) class RNGen
import RNGen
console.log(π);
// (alias) const π: 3.14
// import π
你可以接受所有的匯出物件,然後使用 * as name
把它們放入一個單獨的名稱空間:
// @filename: app.ts
import * as math from "./maths.js";
console.log(math.pi);
const positivePhi = math.absolute(math.phi);
// const positivePhi: number
你可以通過 import "./file"
匯入一個檔案,這不會引用任何變數到你當前模組:
// @filename: app.ts
import "./maths.js";
console.log("3.14");
在這個例子中, import
什麼也沒幹,然而,math.ts
的所有程式碼都會執行,觸發一些影響其他物件的副作用(side-effects)。
TypeScript 具體的 ES 模組語法(TypeScript Specific ES Module Syntax)
型別可以像 JavaScript 值那樣,使用相同的語法被匯出和匯入:
// @filename: animal.ts
export type Cat = { breed: string; yearOfBirth: number };
export interface Dog {
breeds: string[];
yearOfBirth: number;
}
// @filename: app.ts
import { Cat, Dog } from "./animal.js";
type Animals = Cat | Dog;
TypeScript 已經在兩個方面擴充了 import
語法,方便型別匯入:
匯入型別(import type)
// @filename: animal.ts
export type Cat = { breed: string; yearOfBirth: number };
// 'createCatName' cannot be used as a value because it was imported using 'import type'.
export type Dog = { breeds: string[]; yearOfBirth: number };
export const createCatName = () => "fluffy";
// @filename: valid.ts
import type { Cat, Dog } from "./animal.js";
export type Animals = Cat | Dog;
// @filename: app.ts
import type { createCatName } from "./animal.js";
const name = createCatName();
內建型別匯入(Inline type imports)
TypeScript 4.5 也允許單獨的匯入,你需要使用 type
字首 ,表明被匯入的是一個型別:
// @filename: app.ts
import { createCatName, type Cat, type Dog } from "./animal.js";
export type Animals = Cat | Dog;
const name = createCatName();
這些可以讓一個非 TypeScript 編譯器比如 Babel、swc 或者 esbuild 知道什麼樣的匯入可以被安全移除。
匯入型別和內建型別匯入的區別在於一個是匯入語法,一個是僅僅匯入型別。
有 CommonJS 行為的 ES 模組語法(ES Module Syntax with CommonJS Behavior)
TypeScript 之所以有 ES 模組語法跟 CommonJS 和 AMD 的 required
有很大的關係。使用 ES 模組語法的匯入跟 require
一樣都可以處理絕大部分的情況,但是這個語法能確保你在有 CommonJS 輸出的 TypeScript 檔案裡,有一個 1 對 1 的匹配:
import fs = require("fs");
const code = fs.readFileSync("hello.ts", "utf8");
你可以在模組引用頁面瞭解到關於這個語法更多的資訊。
CommonJS 語法(CommonJS Syntax)
CommonJS 是 npm 大部分模組的格式。即使你正在寫 ES 模組語法,瞭解一下 CommonJS 語法的工作原理也會幫助你除錯更容易。
匯出(Exporting)
通過設定全域性 module
的 exports
屬性,匯出識別符號。
function absolute(num: number) {
if (num < 0) return num * -1;
return num;
}
module.exports = {
pi: 3.14,
squareTwo: 1.41,
phi: 1.61,
absolute,
};
這些檔案可以通過一個 require
語句匯入:
const maths = require("maths");
maths.pi;
// any
你可以使用 JavaScript 的解構語法簡化一點程式碼:
const { squareTwo } = require("maths");
squareTwo;
// const squareTwo: any
CommonJS 和 ES 模組互操作(CommonJS and ES Modules interop)
因為預設匯出和模組宣告空間物件匯出的差異,CommonJS 和 ES 模組不是很合適一起使用。TypeScript 有一個 esModuleInterop 編譯選項可以減少兩種規範之間的衝突。
TypeScript 模組解析選項(TypeScript’s Module Resolution Options)
模組解析是從 import
或者 require
語句中取出字串,然後決定字元指向的是哪個檔案的過程。
TypeScript 包含兩個解析策略:Classic 和 Node。Classic,當編譯選項 module 不是 commonjs
時的預設選擇,包含了向後相容。Node 策略則複製了 CommonJS 模式下 Nodejs 的執行方式,會對 .ts
和 .d.ts
有額外的檢查。
這裡有很多 TSConfig 標誌可以影響 TypeScript 的模組策略:moduleResolution, baseUrl, paths, rootDirs。
關於這些策略工作的完整細節,你可以參考 Module Resolution。
TypeScript 模組輸出選項(TypeScript’s Module Output Options)
有兩個選項可以影響 JavaScript 輸出的檔案:
你使用哪個 target 取決於你期望程式碼執行的環境。這些可以是:你需要支援的最老的瀏覽器,你期望程式碼執行的最老的 Nodejs 版本,或者一些獨特的執行環境比如 Electron 等。
編譯選項 module 決定了模組之間通訊使用哪一種規範。在執行時,模組載入器會在執行模組之前,查詢並執行這個模組所有的依賴。
舉個例子,這是一個使用 ES Module 語法的 TypeScript 檔案,展示了 module 選項不同導致的編譯結果:
import { valueOfPi } from "./constants.js";
export const twoPi = valueOfPi * 2;
ES2020
import { valueOfPi } from "./constants.js";
export const twoPi = valueOfPi * 2;
CommonJS
"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
(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 模組頁面看到所有可用的選項和它們對應編譯後的 JavaScript 程式碼長什麼樣。
TypeScript 名稱空間(TypeScript namespaces)
TypeScript 有它自己的模組格式,名為 namespaces
。它在 ES 模組標準之前出現。這個語法有一系列的特性,可以用來建立複雜的定義檔案,現在依然可以在 DefinitelyTyped 看到。當沒有被廢棄的時候,名稱空間主要的特性都還存在於 ES 模組,我們推薦你對齊 JavaScript 方向使用。你可以在名稱空間頁面瞭解更多。
TypeScript 系列
TypeScript 系列文章由官方文件翻譯、重難點解析、實戰技巧三個部分組成,涵蓋入門、進階、實戰,旨在為你提供一個系統學習 TS 的教程,全系列預計 40 篇左右。點此瀏覽全系列文章,並建議順便收藏站點。
微信:「mqyqingfeng」,加我進冴羽唯一的讀者群。
如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎 star,對作者也是一種鼓勵。