我正在編寫一個應用程式
單個 tsconfig.json 只能表示一種環境,無論是就可用的全域性變數而言,還是就模組的行為方式而言。如果你的應用程式包含伺服器程式碼、DOM 程式碼、Web Worker 程式碼、測試程式碼以及所有這些程式碼共享的程式碼,那麼每一部分都應該擁有自己的 tsconfig.json,並透過 專案引用 (project references) 連線起來。然後,為每個 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 嘗試與可用於在 Node.js 中編譯和執行 JS 輸出的程式碼及 tsconfig.json 設定保持相容。更多詳細資訊,請參閱 ts-node 文件。
我正在使用 tsx
雖然 ts-node 預設對 Node.js 的模組系統進行最小程度的修改,但 tsx 的行為更像打包工具,允許使用無後綴/索引模組說明符,並支援 ESM 和 CJS 的任意混合。請對 tsx 使用與為打包工具所用相同的設定。
我正在為瀏覽器編寫 ES 模組,且不使用打包工具或模組編譯器
TypeScript 目前沒有專門針對此場景的選項,但您可以透過結合使用 nodenext ESM 模組解析演算法和 paths 來替代 URL 和 import map 支援,從而近似實現該場景。
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 中解析的匯入報錯。
tsimport {} from "lodash";// ^^^^^^^^// File '/project/empty-file.ts' is not a module. ts(2306)
或者,您可以使用 import maps 在瀏覽器中將一組裸說明符明確對映到 URL,同時依賴 nodenext 的預設 node_modules 查詢,或使用 paths 將 TypeScript 指向這些裸說明符匯入的型別宣告檔案。
html<script type="importmap">{"imports": {"lodash": "https://esm.sh/lodash@4.17.21"}}</script>
tsimport {} from "lodash";// Browser: https://esm.sh/lodash@4.17.21// TypeScript: ./node_modules/@types/lodash/index.d.ts
我正在編寫一個庫
作為庫作者選擇編譯設定,與作為應用作者選擇設定是根本不同的過程。在編寫應用程式時,選擇的設定反映了執行時環境或打包工具(通常是具有已知行為的單一實體)。在編寫庫時,您理想情況下應該在所有可能的庫使用者編譯設定下檢查您的程式碼。由於這不切實際,您可以改用盡可能嚴格的設定,因為滿足這些設定通常也滿足所有其他設定。
json{"compilerOptions": {"module": "node18","target": "es2020", // set to the *lowest* target you support"strict": true,"verbatimModuleSyntax": true,"declaration": true,"sourceMap": true,"declarationMap": true,"rootDir": "src","outDir": "dist"}}
讓我們看看為什麼要選擇其中每一項設定。
-
module: "node18"。當代碼庫與 Node.js 的模組系統相容時,它幾乎總是也能在打包工具中執行。如果您使用第三方發射器來輸出 ESM,請確保在 package.json 中設定"type": "module",以便 TypeScript 將您的程式碼作為 ESM 進行檢查,該檢查在 Node.js 中使用比 CommonJS 更嚴格的模組解析演算法。作為一個示例,讓我們看看如果一個庫使用"moduleResolution": "bundler"編譯會發生什麼。tsexport * from "./utils";假設存在
./utils.ts(或./utils/index.ts),打包工具對這段程式碼不會有問題,因此"moduleResolution": "bundler"不會報錯。使用"module": "esnext"編譯時,此 export 語句的輸出 JavaScript 將看起來與輸入完全一樣。如果該 JavaScript 被髮布到 npm,它將可供使用打包工具的專案使用,但當在 Node.js 中執行時,它會導致錯誤。Error [ERR_MODULE_NOT_FOUND]: Cannot find module '.../node_modules/dependency/utils' imported from .../node_modules/dependency/index.jsDid you mean to import ./utils.js?另一方面,如果我們這樣寫:
tsexport * from "./utils.js";這將產生既能在 Node.js 中執行,又能在打包工具中執行的輸出。
簡而言之,
"moduleResolution": "bundler"具有傳染性,允許產生僅在打包工具中有效的程式碼。同樣,"moduleResolution": "nodenext"僅檢查輸出是否在 Node.js 中有效,但在大多數情況下,在 Node.js 中有效的模組程式碼也將在其他執行時和打包工具中有效。 -
target: "es2020"。將此值設定為您打算支援的最低 ECMAScript 版本,可確保發出的程式碼不會使用更高版本中引入的語言特性。由於target也隱含了lib的對應值,這也確保您不會訪問在舊環境中可能不可用的全域性變數。 -
strict: true。如果沒有這個設定,您可能會編寫最終進入輸出.d.ts檔案並導致使用者在啟用strict的情況下編譯時報錯的型別級程式碼。例如,這個extends子句tsexport interface Super {foo: string;}export interface Sub extends Super {foo: string | undefined;}僅在
strictNullChecks下是一個錯誤。另一方面,很難編寫僅在 停用strict時才報錯的程式碼,因此強烈建議庫使用strict進行編譯。 -
verbatimModuleSyntax: true。此設定可以防止一些可能導致庫使用者問題的模組相關陷阱。首先,它阻止編寫任何可能根據使用者的esModuleInterop或allowSyntheticDefaultImports值而產生歧義的匯入語句。此前,通常建議庫在不使用esModuleInterop的情況下編譯,因為在庫中使用它可能會迫使使用者也採用它。然而,編寫僅在沒有esModuleInterop的情況下有效的匯入也是可能的,因此該設定的任何值都不能保證庫的可移植性。verbatimModuleSyntax確實提供了這樣的保證。1 其次,它阻止在將被髮射為 CommonJS 的模組中使用export default,這可能要求打包工具使用者和 Node.js ESM 使用者以不同方式使用該模組。有關更多詳細資訊,請參閱關於 ESM/CJS 互操作 的附錄。 -
declaration: true會在輸出的 JavaScript 旁邊發出型別宣告檔案。庫的使用者需要這些檔案才能獲得任何型別資訊。 -
sourceMap: true和declarationMap: true分別為輸出的 JavaScript 和型別宣告檔案發出原始碼對映。這些僅在庫也釋出其原始碼 (.ts) 檔案時才有用。透過釋出原始碼對映和原始碼檔案,庫的使用者能夠更容易地除錯庫程式碼。透過釋出宣告對映和原始碼檔案,使用者在對來自庫的匯入執行“跳轉到定義”操作時,將能夠看到原始的 TypeScript 原始碼。這兩者都代表了開發者體驗和庫大小之間的權衡,因此是否包含它們由您決定。 -
rootDir: "src"和outDir: "dist"。使用單獨的輸出目錄總是一個好主意,但對於釋出其輸入檔案的庫來說,這是必要的。否則,副檔名替換將導致庫的使用者載入庫的.ts檔案而不是.d.ts檔案,從而導致型別錯誤和效能問題。
捆綁庫的注意事項
如果您正在使用打包工具來輸出您的庫,那麼您所有的(非外部化的)匯入都將由具有已知行為的打包工具處理,而不是由您使用者的未知環境處理。在這種情況下,您可以使用 "module": "esnext" 和 "moduleResolution": "bundler",但需注意兩個注意事項:
-
當某些檔案被捆綁而另一些檔案被外部化時,TypeScript 無法對模組解析進行建模。在捆綁帶有依賴項的庫時,通常將第一方庫的原始碼捆綁到單個檔案中,但將外部依賴項的匯入保留為捆綁輸出中的真實匯入。這本質上意味著模組解析被拆分在打包工具和終端使用者的環境之間。為了在 TypeScript 中對此建模,您需要使用
"moduleResolution": "bundler"處理捆綁匯入,並使用"moduleResolution": "nodenext"處理外部化匯入(或者使用多個選項來檢查一切是否在一定範圍內的終端使用者環境中工作)。但 TypeScript 無法配置為在同一次編譯中使用兩種不同的模組解析設定。因此,使用"moduleResolution": "bundler"可能會允許在打包工具中工作但在 Node.js 中不安全的外部化依賴項匯入。另一方面,使用"moduleResolution": "nodenext"可能會對捆綁匯入施加過於嚴格的要求。 -
您必須確保您的宣告檔案也被捆綁。回顧宣告檔案的第一條規則:每個宣告檔案僅代表一個 JavaScript 檔案。如果您使用
"moduleResolution": "bundler"並使用打包工具輸出 ESM 捆綁包,同時使用tsc輸出許多獨立的宣告檔案,那麼當在"module": "nodenext"下使用時,您的宣告檔案可能會導致錯誤。例如,像這樣的輸入檔案tsimport { Component } from "./extensionless-relative-import";它的匯入會被 JS 打包工具擦除,但會產生一個帶有相同匯入語句的宣告檔案。然而,該匯入語句將在 Node.js 中包含無效的模組說明符,因為它缺少副檔名。對於 Node.js 使用者,TypeScript 將在宣告檔案上報錯,並將引用
Component的型別設為any,假設該依賴項將在執行時崩潰。如果您的 TypeScript 打包工具不能生成捆綁的宣告檔案,請使用
"moduleResolution": "nodenext"以確保保留在宣告檔案中的匯入與終端使用者的 TypeScript 設定相容。更好的是,考慮不要捆綁您的庫。
關於雙重輸出解決方案的說明
單次 TypeScript 編譯(無論是輸出還是僅進行型別檢查)都假定每個輸入檔案只會產生一個輸出檔案。即使 tsc 沒有輸出任何內容,它對匯入名稱執行的型別檢查也依賴於對輸出檔案在執行時如何表現的瞭解,這基於 tsconfig.json 中設定的模組和發射相關的選項。雖然只要 tsc 可以配置為理解其他發射器將發出什麼,第三方發射器通常可以安全地與 tsc 型別檢查結合使用,但任何只進行一次型別檢查卻發出兩組具有不同模組格式的輸出的解決方案,都會讓(至少)其中一個輸出未被檢查。由於外部依賴項可能會向 CommonJS 和 ESM 使用者公開不同的 API,因此沒有配置可以保證在單次編譯中兩者輸出都型別安全。在實踐中,大多數依賴項都遵循最佳實踐,雙重輸出解決方案是有效的。在釋出之前對所有輸出捆綁包執行測試和 靜態分析 可顯著降低嚴重問題未被發現的可能性。
verbatimModuleSyntax只有在 JS 發射器發射的模組種類與tsc在給定 tsconfig.json、原始檔副檔名和 package.json"type"時所發射的模組種類相同時才能工作。該選項的工作方式是強制執行所編寫的import/require與發射的import/require完全一致。任何從同一個原始檔同時產生 ESM 和 CJS 輸出的配置都從根本上與verbatimModuleSyntax不相容,因為其全部目的就是為了防止您在任何會被髮射為require的地方編寫import。verbatimModuleSyntax也可以透過將第三方發射器配置為發射與tsc不同的模組種類來破解——例如,透過在 tsconfig.json 中設定"module": "esnext",同時配置 Babel 以發射 CommonJS。↩