模組 - 選擇編譯器選項

我正在撰寫應用程式

單一 tsconfig.json 只能代表單一環境,無論是在可用全域變數或模組行為方面。如果你的應用程式包含伺服器程式碼、DOM 程式碼、網頁工作執行緒程式碼、測試程式碼,以及所有這些程式碼共用的程式碼,每個程式碼都應該有自己的 tsconfig.json,並透過 專案參考 進行連線。然後,針對每個 tsconfig.json 使用本指南一次。對於應用程式內的類別庫專案,特別是需要在多個執行時間環境中執行的專案,請使用「我正在撰寫類別庫」區段。

我正在使用套件管理工具

除了採用下列設定之外,也建議不要在套件管理工具專案中設定 { "type": "module" } 或使用 .mts 檔案。在這些情況下,某些套件管理工具 會採用不同的 ESM/CJS 互通行為,而 TypeScript 目前無法使用 "moduleResolution": "bundler" 進行分析。請參閱 問題 #54102 以取得更多資訊。

json
{
"compilerOptions": {
// This is not a complete template; it only
// shows relevant module-related settings.
// Be sure to set other important options
// like `target`, `lib`, and `strict`.
// Required
"module": "esnext",
"moduleResolution": "bundler",
"esModuleInterop": true,
// Consult your bundler’s documentation
"customConditions": ["module"],
// Recommended
"noEmit": true, // or `emitDeclarationOnly`
"allowImportingTsExtensions": true,
"allowArbitraryExtensions": true,
"verbatimModuleSyntax": true, // or `isolatedModules`
}
}

我在 Node.js 中編譯並執行輸出

如果您打算發射 ES 模組,請記得設定 "type": "module" 或使用 .mts 檔案。

json
{
"compilerOptions": {
// This is not a complete template; it only
// shows relevant module-related settings.
// Be sure to set other important options
// like `target`, `lib`, and `strict`.
// Required
"module": "nodenext",
// Implied by `"module": "nodenext"`:
// "moduleResolution": "nodenext",
// "esModuleInterop": true,
// "target": "esnext",
// Recommended
"verbatimModuleSyntax": true,
}
}

我在使用 ts-node

ts-node 嘗試與相同的程式碼和相同的 tsconfig.json 設定相容,這些設定可用於 在 Node.js 中編譯並執行 JS 輸出。請參閱 ts-node 文件 以取得更多詳細資訊。

我在使用 tsx

雖然 ts-node 預設會對 Node.js 的模組系統進行最小的修改,但 tsx 的行為更像是一個打包器,允許不帶副檔名/索引模組的指定符,以及 ESM 和 CJS 的任意混合。對 tsx 使用與 打包器相同的設定

我在為瀏覽器撰寫 ES 模組,沒有使用打包器或模組編譯器

TypeScript 目前沒有專門針對此情境的選項,但你可以使用 nodenext ESM 模組解析演算法和 paths 的組合,作為 URL 和匯入地圖支援的替代方案。

json
// tsconfig.json
{
"compilerOptions": {
// This is not a complete template; it only
// shows relevant module-related settings.
// Be sure to set other important options
// like `target`, `lib`, and `strict`.
// Combined with `"type": "module"` in a local package.json,
// this enforces including file extensions on relative path imports.
"module": "nodenext",
"paths": {
// Point TS to local types for remote URLs:
"https://esm.sh/lodash@4.17.21": ["./node_modules/@types/lodash/index.d.ts"],
// Optional: point bare specifier imports to an empty file
// to prohibit importing from node_modules specifiers not listed here:
"*": ["./empty-file.ts"]
}
}
}

此設定允許明確列出的 HTTPS 匯入使用本機安裝的類型宣告檔案,同時對會在 node_modules 中解析的匯入產生錯誤。

ts
import {} from "lodash";
// ^^^^^^^^
// File '/project/empty-file.ts' is not a module. ts(2306)

或者,你可以使用 匯入地圖 在瀏覽器中明確將一組裸指定符對應到 URL,同時依賴 nodenext 的預設 node_modules 查詢,或依賴 paths,將 TypeScript 引導至這些裸指定符匯入的類型宣告檔案。

html
<script type="importmap">
{
"imports": {
"lodash": "https://esm.sh/lodash@4.17.21"
}
}
</script>
ts
import {} from "lodash";
// Browser: https://esm.sh/lodash@4.17.21
// TypeScript: ./node_modules/@types/lodash/index.d.ts

我在撰寫一個函式庫

身為函式庫作者選擇編譯設定,與身為應用程式作者選擇設定的過程截然不同。撰寫應用程式時,選擇的設定會反映執行時期環境或套件管理員,通常是具有已知行為的單一實體。撰寫函式庫時,理想情況下,您會在所有可能的函式庫使用者編譯設定下檢查您的程式碼。由於這不切實際,因此您可以改用最嚴格的可能設定,因為滿足這些設定往往會滿足所有其他設定。

json
{
"compilerOptions": {
"module": "node16",
"target": "es2020", // set to the *lowest* target you support
"strict": true,
"verbatimModuleSyntax": true,
"declaration": true,
"sourceMap": true,
"declarationMap": true
}
}

讓我們探討我們選擇這些設定的理由

  • module: "node16"。當程式碼庫與 Node.js 的模組系統相容時,它幾乎總能與套件管理員搭配使用。如果您使用第三方發射器來發射 ESM 輸出,請確保在您的 package.json 中設定 "type": "module",以便 TypeScript 將您的程式碼檢查為 ESM,它在 Node.js 中使用比 CommonJS 更嚴格的模組解析演算法。舉例來說,讓我們看看如果函式庫使用 "moduleResolution": "bundler" 編譯會發生什麼事

    ts
    export * from "./utils";

    假設存在 ./utils.ts(或 ./utils/index.ts),套件管理員會接受這個程式碼,因此 "moduleResolution": "bundler" 沒有抱怨。使用 "module": "esnext" 編譯,這個匯出陳述式的輸出 JavaScript 看起來會與輸入完全相同。如果將該 JavaScript 發布到 npm,使用套件管理員的專案可以使用它,但在 Node.js 中執行時會造成錯誤

    Error [ERR_MODULE_NOT_FOUND]: Cannot find module '.../node_modules/dependency/utils' imported from .../node_modules/dependency/index.js Did you mean to import ./utils.js?

    另一方面,如果我們寫成

    ts
    export * from "./utils.js";

    這會產生在 Node.js 套件管理員中都能運作的輸出。

    簡而言之,"moduleResolution": "bundler" 具有傳染性,允許產生僅在打包器中運作的程式碼。同樣地,"moduleResolution": "nodenext" 僅檢查輸出是否在 Node.js 中運作,但在大多數情況下,在 Node.js 中運作的模組程式碼也會在其他執行時間和打包器中運作。

  • target: "es2020"。將此值設定為您打算支援的最低 ECMAScript 版本,以確保發出的程式碼不會使用後續版本中引入的語言功能。由於 target 也暗示 lib 的對應值,這也確保您不會存取在較舊環境中可能無法使用的全域變數。

  • strict: true。沒有這項設定,您可能會撰寫類型層級的程式碼,最後會出現在輸出的 .d.ts 檔案中,並在使用者在啟用 strict 的情況下編譯時發生錯誤。例如,這個 extends 子句

    ts
    export interface Super {
    foo: string;
    }
    export interface Sub extends Super {
    foo: string | undefined;
    }

    僅在 strictNullChecks 下會發生錯誤。另一方面,撰寫僅在 strict 停用 時才會發生錯誤的程式碼非常困難,因此強烈建議函式庫使用 strict 編譯。

  • verbatimModuleSyntax: true。此設定可以防止一些與模組相關的陷阱,這些陷阱可能會對函式庫使用者造成問題。首先,它可以防止撰寫任何匯入陳述式,這些陳述式可能會根據使用者對 esModuleInteropallowSyntheticDefaultImports 的值而產生歧義的詮釋。以前,通常建議函式庫在沒有 esModuleInterop 的情況下編譯,因為在函式庫中使用它可能會迫使用戶也採用它。但是,也可以撰寫僅在 沒有 esModuleInterop 的情況下運作的匯入,因此設定的兩個值都不保證函式庫的可移植性。verbatimModuleSyntax 確實提供了這樣的保證。1其次,它可以防止在將發射為 CommonJS 的模組中使用 export default,這可能需要套件管理程式使用者和 Node.js ESM 使用者以不同的方式使用模組。有關更多詳細資訊,請參閱 ESM/CJS Interop 附錄。

  • declaration: true 會在輸出 JavaScript 旁邊發射類型宣告檔案。函式庫使用者需要此檔案才能取得任何類型資訊。

  • sourceMap: truedeclarationMap: true 分別為輸出 JavaScript 和類型宣告檔案發射原始碼對應。這些僅在函式庫也發布其原始碼 (.ts) 檔案時才有用。透過發布原始碼對應和原始碼檔案,函式庫使用者將能夠更容易地除錯函式庫程式碼。透過發布宣告對應和原始碼檔案,使用者將能夠在從函式庫匯入時執行「前往定義」時看到原始 TypeScript 原始碼。這兩者都代表了開發人員體驗和函式庫大小之間的權衡,因此是否包含它們取決於您。

套件管理程式函式庫的考量因素

如果您使用 bundler 發射程式庫,則所有(非外部化)匯入都會由 bundler 處理,其行為已知,而不是由使用者的未知環境處理。在這種情況下,您可以使用 "module": "esnext""moduleResolution": "bundler",但僅限於兩個警告

  1. 當某些檔案已打包而某些檔案已外部化時,TypeScript 無法建構模組解析。在將包含依賴項的程式庫打包時,通常會將第一方程式庫原始碼打包成單一檔案,但將外部依賴項的匯入保留為打包輸出中的實際匯入。這基本上表示模組解析會在 bundler 和最終使用者的環境之間分割。若要在 TypeScript 中建構此模型,您會希望使用 "moduleResolution": "bundler" 處理已打包的匯入,並使用 "moduleResolution": "nodenext"(或使用多個選項來檢查所有內容是否會在範圍內運作)處理外部化的匯入。最終使用者環境)。但是,無法將 TypeScript 設定為在同一個編譯中使用兩個不同的模組解析設定。因此,使用 "moduleResolution": "bundler" 可能允許匯入外部化依賴項,這些依賴項可以在 bundler 中運作,但在 Node.js 中不安全。另一方面,使用 "moduleResolution": "nodenext" 可能對已打包的匯入施加過於嚴格的要求。

  2. 您必須確保您的宣告檔案也已打包。回想一下 宣告檔案的第一個規則:每個宣告檔案都只代表一個 JavaScript 檔案。如果您使用 "moduleResolution": "bundler" 並使用 bundler 發射 ESM 套件,同時使用 tsc 發射許多個別宣告檔案,則您的宣告檔案在 "module": "nodenext" 下使用時可能會導致錯誤。例如,像這樣的輸入檔案

    ts
    import { Component } from "./extensionless-relative-import";

    其匯入會被 JS bundler 刪除,但會產生具有相同匯入陳述的宣告檔案。然而,該匯入陳述在 Node.js 中會包含無效的模組指定符,因為它缺少檔案副檔名。對於 Node.js 使用者,TypeScript 會對宣告檔案產生錯誤,並使用 any 感染參照 Component 的類型,假設依賴項在執行階段會崩潰。

    如果您的 TypeScript 捆綁器未產生捆綁宣告檔,請使用 "moduleResolution": "nodenext" 確保宣告檔中保留的匯入與終端使用者的 TypeScript 設定相容。更好的做法是,考慮不要捆綁您的函式庫。

雙重發射解決方案注意事項

單一 TypeScript 編譯(無論是發射或僅類型檢查)假設每個輸入檔只會產生一個輸出檔。即使 tsc 沒有發射任何內容,它對匯入名稱執行的類型檢查依賴於對 tsconfig.json 中設定的模組和發射相關選項,了解輸出檔在執行階段的行為。雖然第三方發射器通常可以安全地與 tsc 類型檢查結合使用,只要 tsc 可以設定為了解其他發射器會發射什麼,任何在僅類型檢查一次的情況下發射兩組不同輸出(具有不同模組格式)的解決方案都會讓(至少)其中一個輸出未經檢查。由於外部依賴項可能會對 CommonJS 和 ESM 使用者公開不同的 API,因此您無法在單一編譯中使用任何設定來保證兩個輸出都是類型安全的。實際上,大多數依賴項都遵循最佳實務,而且雙重發射輸出可行。在發布前針對所有輸出套件執行測試和 靜態分析 可大幅降低嚴重問題未被發現的機率。


  1. verbatimModuleSyntax 僅在 JS 發射器發射與 tsc 給定的 tsconfig.json、原始檔副檔名和 package.json "type" 相同的模組類型時才能運作。此選項透過強制執行的 import/require 與發射的 import/require 相同來運作。任何同時從相同原始檔產生 ESM 和 CJS 輸出的組態基本上與 verbatimModuleSyntax 不相容,因為它的整體目的就是防止您在會發射 require 的任何地方撰寫 importverbatimModuleSyntax 也可能因將第三方發射器組態為發射與 tsc 不同的模組類型而失效,例如在 tsconfig.json 中設定 "module": "esnext",同時將 Babel 組態為發射 CommonJS。

TypeScript 文件是一個開放原始碼專案。透過 傳送 Pull Request ❤ 來協助我們改善這些頁面

此頁面的貢獻者
ABAndrew Branch (6)

最後更新:2024 年 3 月 21 日