TypeScript 官方手冊翻譯計劃【十三】:模組

Chor發表於2021-12-16
  • 說明:目前網上沒有 TypeScript 最新官方文件的中文翻譯,所以有了這麼一個翻譯計劃。因為我也是 TypeScript 的初學者,所以無法保證翻譯百分之百準確,若有錯誤,歡迎評論區指出;
  • 翻譯內容:暫定翻譯內容為 TypeScript Handbook,後續有空會補充翻譯文件的其它部分;
  • 專案地址TypeScript-Doc-Zh,如果對你有幫助,可以點一個 star ~

本章節官方文件地址:Modules

模組

處理模組化程式碼的方式很多,JavaScript 在這方面有著悠久的歷史。TypeScript 誕生於 2012 年,對許多模組化方案也實現了支援。但隨著時間的推移,社群和 JavaScript 規範在一種名為 ES 模組(或者稱為 ES6 模組)的方案上達成了共識。你可能聽說過它的 import/export 語法。

ES 模組於 2015 年被納入 JavaScript 規範,到了 2020 年,它已經獲得了多數 web 瀏覽器和 JavaScript 執行時的支援。

本手冊會重點講解 ES 模組以及在它之前非常流行的、提供了 module.exports = 語法的 CommonJS。在“參考”章節的模組這一小節中,你可以瞭解到更多關於其它模組化方案的資訊。

JavaScript 的模組是如何定義的

和 ECMAScript 2015 一樣,TypeScript 會將任何包含頂層 import 或者 export 的檔案視為一個模組。

反過來,一個不包含頂層 import 或者 export 宣告的檔案會被視為一個指令碼,它的內容可以在全域性作用域中訪問到(因此對模組也是可見的)。

模組在自身的作用域而非全域性作用域中執行。這意味著在一個模組中宣告的變數、函式和類等在模組外面是不可見的,除非使用其中一種匯出方式將它們顯式匯出。反過來,為了使用從某個不同的模組中匯出的變數、函式、類等,也需要使用其中一種匯入方式將它們匯入。

非模組

在我們開始之前,有個很重要的事情需要搞清楚,那就是 TypeScript 會將什麼視為一個模組。JavaScript 規範表明,任何不包含 export 或者頂層 await 的 JavaScript 檔案都應該被視為一個指令碼,而不是一個模組。

在一個指令碼檔案中宣告的變數和型別會位於共享的全域性作用域中,而且通常情況下,你會使用 outFile 編譯選項將多個輸入檔案合併為一個輸出檔案,或者使用 HTML 檔案中的多個 <script> 標籤去(以正確的順序!)載入檔案。

如果你的檔案當前沒有任何的 import 或者 export,但是你想將其視為一個模組,那麼可以新增下面這行程式碼:

export {};

這會將檔案轉化為沒有匯出任何東西的一個模組。不管你的模組目標是什麼,這個語法都可以生效。

TypeScript 中的模組

在 TypeScript 中編寫基於模組的程式碼時,有三件主要的事情需要考慮:

  • 語法:我想要使用什麼語法去進行匯入和匯出?
  • 模組解析:模組名(或者路徑)和磁碟上的檔案有什麼關係?
  • 模組輸出目標:產生的 JavaScript 模組看起來應該是什麼樣子的?

ES 模組語法

一個檔案可以通過 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

其它匯入語法

可以使用諸如 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 中的所有程式碼都會被執行,這可能會觸發副作用並影響其它物件。

TypeScript 專屬的 ES 模組語法

你可以使用和 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();

內聯 type 匯入

TypeScript 4.5 也允許單個匯入使用 type 字首表明匯入的引用是一個型別:

// @filename: app.ts
import { createCatName, type Cat, type Dog } from "./animal.js";
 
export type Animals = Cat | Dog;
const name = createCatName();

所有這些都允許諸如 Babel、swc 或者 esbuild 這樣的非 TypeScript 轉譯工具知道哪些匯入是可以被安全地移除的。

具備 CommonJS 行為的 ES 模組語法

TypeScript 的 ES 模組語法可以和 CommonJS 與 AMD 的 require 直接關聯。在大多數情況下,使用 ES 模組的匯入與相同環境下使用 require 是一樣的,但這個語法可以確保你的 TypeScript 檔案和 CommonJS 輸出存在一對一的匹配:

import fs = require("fs");
const code = fs.readFileSync("hello.ts", "utf8");

你可以在模組這一參考章節中瞭解到更多關於該語法的資訊。

CommonJS 語法

CommonJS 是大多數 npm 包採用的模組化方案。即使你編寫程式碼的時候採用的是 ES 模組語法,簡要了解一下 CommonJS 語法的工作方式也有助於簡化你的除錯過程。

匯出

通過給一個名為 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 和 ES 模組在功能上存在不匹配的地方。TypeScript 提供了一個編譯選項 esModuleInterop,以減少這兩組不同的約束之間的衝突。

TypeScript 的模組解析選項

模組解析是一個過程,它指的是從 import 或者 require 宣告中提取一個字串,並確定該字串所指示的檔案。

TypeScript 使用兩種解析策略:Classic 和 Node。使用 Classic 策略是為了實現向後相容,當編譯選項 module 不是 commonjs 的時候,預設採用該策略。Node 策略則復刻了 Node.js 在 CommonJS 模式下的工作方式,並提供了額外的 .ts.d.ts 檢查。

還有很多 TSConfig 選項會影響到 TypeScript 中的模組策略,包括:moduleResolutionbaseUrlpathsrootDirs

有關這些策略如何工作的詳細資訊,請閱讀模組解析

TypeScript 的模組輸出選項

有兩個選項會影響到最終輸出的 JavaScript:

  • target 會決定哪些 JS 特性會被降級(進行轉化並執行在比較舊的 JavaScript 執行時),哪些 JS 特性會被保留
  • module 會決定模組之間進行互動所使用的程式碼

使用哪個 target,取決於你希望執行 TypeScript 程式碼的 JavaScript 執行時可以使用的特性。這樣的執行時可以是:你支援的最舊的瀏覽器,你希望可以執行的最低版本的 Node.js,或者從執行時 —— 比如 Electron 的唯一約束進行考量。

模組之間的所有通訊通過一個模組載入器進行,編譯選項 module 會決定應該使用哪一個。在執行時,模組載入器負責在執行模組之前定位並執行模組的所有依賴。

舉個例子,這是一個使用 ES 模組語法的 TypeScript 檔案:

import { valueOfPi } from "./constants.js";
 
export const twoPi = valueOfPi * 2;

下面是使用不同的 module 選項之後的編譯結果:

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 有自己的模組格式,名為“名稱空間”,它比 ES 模組標準出現得要早。這個語法提供了很多有用的特性以建立複雜的定義檔案,並且仍然廣泛應用於 DefinitelyTyped 中。雖然該語法還沒有被棄用,但鑑於 ES 模組已經擁有了名稱空間的大部分特性,我們推薦你使用 ES 模組來跟 JavaScript 保持一致。在名稱空間的參考章節中,你可以瞭解到更多相關資訊。

相關文章