模組語法
TypeScript 編譯器會在 TypeScript 和 JavaScript 檔案中辨識標準的 ECMAScript 模組語法,以及在 JavaScript 檔案中辨識許多形式的 CommonJS 語法。
另外還有幾個 TypeScript 專屬的語法擴充,可以用在 TypeScript 檔案和/或 JSDoc 註解中。
匯入和匯出 TypeScript 專屬宣告
型別別名、介面、列舉和命名空間可以從模組中使用 export 修飾詞匯出,就像任何標準 JavaScript 宣告一樣
ts// Standard JavaScript syntax...export function f() {}// ...extended to type declarationsexport type SomeType = /* ... */;export interface SomeInterface { /* ... */ }
它們也可以在命名匯出中被參照,甚至可以與標準 JavaScript 宣告的參照並列
tsexport { f, SomeType, SomeInterface };
匯出的型別(和其他 TypeScript 專屬宣告)可以使用標準 ECMAScript 匯入
tsimport { f, SomeType, SomeInterface } from "./module.js";
使用命名空間匯入或匯出時,匯出的型別會在型別位置被參照時出現在命名空間上
tsimport * as mod from "./module.js";mod.f();mod.SomeType; // Property 'SomeType' does not exist on type 'typeof import("./module.js")'let x: mod.SomeType; // Ok
僅類型匯入和匯出
在將匯入和匯出發射到 JavaScript 時,TypeScript 預設會自動省略(不發射)僅用於類型位置的匯入,以及僅參照類型的匯出。僅類型匯入和匯出可用於強制此行為,並使省略明確。使用 import type 編寫的匯入宣告、使用 export type { ... } 編寫的匯出宣告,以及加上 type 關鍵字前綴的匯入或匯出指定項,都保證會從輸出 JavaScript 中省略。
ts// @Filename: main.tsimport { f, type SomeInterface } from "./module.js";import type { SomeType } from "./module.js";class C implements SomeInterface {constructor(p: SomeType) {f();}}export type { C };// @Filename: main.jsimport { f } from "./module.js";class C {constructor(p) {f();}}
使用 import type 也可以匯入值,但由於它們不會存在於輸出 JavaScript 中,因此只能用於非發射位置
tsimport type { f } from "./module.js";f(); // 'f' cannot be used as a value because it was imported using 'import type'let otherFunction: typeof f = () => {}; // Ok
僅類型匯入宣告不得同時宣告預設匯入和命名繫結,因為 type 適用於預設匯入或整個匯入宣告,這似乎很含糊。請將匯入宣告分成兩個,或使用 default 作為命名繫結
tsimport type fs, { BigIntOptions } from "fs";// ^^^^^^^^^^^^^^^^^^^^^// Error: A type-only import can specify a default import or named bindings, but not both.import type { default as fs, BigIntOptions } from "fs"; // Ok
import() 類型
TypeScript 提供與 JavaScript 的動態 import 類似的類型語法,用於參照模組的類型,而無需撰寫匯入宣告
ts// Access an exported type:type WriteFileOptions = import("fs").WriteFileOptions;// Access the type of an exported value:type WriteFileFunction = typeof import("fs").writeFile;
這在 JavaScript 檔案中的 JSDoc 註解中特別有用,否則無法匯入類型
ts/** @type {import("webpack").Configuration} */module.exports = {// ...}
export = 和 import = require()
在發射 CommonJS 模組時,TypeScript 檔案可以使用 module.exports = ... 和 const mod = require("...") JavaScript 語法的一個直接類比
ts// @Filename: main.tsimport fs = require("fs");export = fs.readFileSync("...");// @Filename: main.js"use strict";const fs = require("fs");module.exports = fs.readFileSync("...");
此語法用於其 JavaScript 對應項,因為變數宣告和屬性指定無法參照 TypeScript 類型,而特殊的 TypeScript 語法可以
ts// @Filename: a.tsinterface Options { /* ... */ }module.exports = Options; // Error: 'Options' only refers to a type, but is being used as a value here.export = Options; // Ok// @Filename: b.tsconst Options = require("./a");const options: Options = { /* ... */ }; // Error: 'Options' refers to a value, but is being used as a type here.// @Filename: c.tsimport Options = require("./a");const options: Options = { /* ... */ }; // Ok
Ambient 模組
TypeScript 支援腳本(非模組)檔案中的語法,用於宣告存在於執行階段但沒有對應檔案的模組。這些Ambient 模組通常代表執行階段提供的模組,例如 Node.js 中的 "fs" 或 "path"
tsdeclare module "path" {export function normalize(p: string): string;export function join(...paths: any[]): string;export var sep: string;}
一旦 Ambient 模組載入到 TypeScript 程式中,TypeScript 將會辨識其他檔案中宣告模組的匯入
ts// 👇 Ensure the ambient module is loaded -// may be unnecessary if path.d.ts is included// by the project tsconfig.json somehow./// <reference path="path.d.ts" />import { normalize, join } from "path";
Ambient 模組宣告很容易與 模組擴充 混淆,因為它們使用相同的語法。當檔案是模組時,此模組宣告語法會變成模組擴充,表示它有頂層 import 或 export 陳述式(或受 --moduleDetection force 或 auto 影響)
ts// Not an ambient module declaration anymore!export {};declare module "path" {export function normalize(p: string): string;export function join(...paths: any[]): string;export var sep: string;}
Ambient 模組可以在模組宣告主體內使用匯入,以參照其他模組,而不會將包含的檔案變成模組(這會讓 Ambient 模組宣告變成模組擴充)
tsdeclare module "m" {// Moving this outside "m" would totally change the meaning of the file!import { SomeType } from "other";export function f(): SomeType;}
模式 Ambient 模組在其名稱中包含單一 * 萬用字元,用於比對匯入路徑中零個或多個字元。這對於宣告自訂載入器提供的模組很有用
tsdeclare module "*.html" {const content: string;export default content;}
module 編譯器選項
此部分討論每個 module 編譯器選項值的詳細資料。請參閱 模組輸出格式 理論部分,以進一步了解選項的背景以及它如何融入整體編譯程序。簡而言之,module 編譯器選項在過去僅用於控制發出 JavaScript 檔案的輸出模組格式。然而,較新的 node16 和 nodenext 值描述了 Node.js 模組系統的廣泛特性,包括支援哪些模組格式、如何決定每個檔案的模組格式,以及不同模組格式如何相互操作。
node16、nodenext
Node.js 支援 CommonJS 和 ECMAScript 模組,針對每個檔案可以採用的格式以及兩種格式如何相互操作,有特定的規則。node16 和 nodenext 描述 Node.js 雙格式模組系統的完整行為範圍,並以 CommonJS 或 ESM 格式發出檔案。這與其他所有 module 選項不同,後者與執行時間無關,並將所有輸出檔案強制轉換成單一格式,讓使用者自行確保輸出對其執行時間有效。
一個常見的誤解是
node16和nodenext只發出 ES 模組。實際上,node16和nodenext描述 Node.js 的版本,這些版本支援 ES 模組,而不仅仅是使用 ES 模組的專案。基於每個檔案的偵測到的模組格式,支援 ESM 和 CommonJS 發出。由於node16和nodenext是唯一反映 Node.js 雙模組系統複雜性的module選項,因此它們是唯一正確的module選項,適用於所有預計在 Node.js v12 或更新版本中執行的應用程式和函式庫,無論它們是否使用 ES 模組。
node16 和 nodenext 目前相同,但例外情況是它們暗示不同的 target 選項值。如果 Node.js 在未來對其模組系統進行重大變更,node16 將會凍結,而 nodenext 將會更新以反映新的行為。
模組格式偵測
.mts/.mjs/.d.mts檔案永遠是 ES 模組。.cts/.cjs/.d.cts檔案永遠是 CommonJS 模組。.ts/.tsx/.js/.jsx/.d.ts檔案是 ES 模組,如果最近的祖先 package.json 檔案包含"type": "module",否則為 CommonJS 模組。
偵測到的輸入 .ts/.tsx/.mts/.cts 檔案模組格式會決定已發出 JavaScript 檔案的模組格式。因此,例如,一個專案完全由 .ts 檔案組成,在 --module nodenext 下預設會發出所有 CommonJS 模組,而且可以透過將 "type": "module" 加入專案 package.json 來發出所有 ES 模組。
互操作性規則
- 當 ES 模組參照 CommonJS 模組時
- 當 CommonJS 模組參照 ES 模組時
require無法參照 ES 模組。對於 TypeScript,這包括在 偵測 為 CommonJS 模組的檔案中的import陳述式,因為那些import陳述式會在已發出的 JavaScript 中轉換為require呼叫。- 可以透過動態
import()呼叫來匯入 ES 模組。它會傳回模組的模組命名空間物件的 Promise(你會從另一個 ES 模組的import * as ns from "./module.js"取得)。
發射
每個檔案的發射格式由每個檔案的 偵測到的模組格式 決定。ESM 發射類似於 --module esnext,但對 import x = require("...") 有特別的轉換,這在 --module esnext 中是不允許的
tsimport x = require("mod");
jsimport { createRequire as _createRequire } from "module";const __require = _createRequire(import.meta.url);const x = __require("mod");
CommonJS 發射類似於 --module commonjs,但動態 import() 呼叫不會被轉換。這裡的發射顯示為已啟用 esModuleInterop
tsimport fs from "fs"; // transformedconst dynamic = import("mod"); // not transformed
js"use strict";var __importDefault = (this && this.__importDefault) || function (mod) {return (mod && mod.__esModule) ? mod : { "default": mod };};Object.defineProperty(exports, "__esModule", { value: true });const fs_1 = __importDefault(require("fs")); // transformedconst dynamic = import("mod"); // not transformed
暗示和強制選項
--module nodenext或node16暗示並強制具有相同名稱的moduleResolution。--module nodenext暗示--target esnext。--module node16暗示--target es2022。--module nodenext或node16暗示--esModuleInterop。
摘要
node16和nodenext是唯一正確的module選項,適用於所有預計在 Node.js v12 或更新版本中執行的應用程式和函式庫,無論它們是否使用 ES 模組。node16和nodenext會根據每個檔案的 偵測到的模組格式 以 CommonJS 或 ESM 格式發射檔案。- Node.js 中 ESM 和 CJS 之間的互用性規則反映在類型檢查中。
- ESM 發射轉換
import x = require("...")至由createRequire匯入所建構的require呼叫。 - CommonJS 發射會讓動態
import()呼叫保持不變,因此 CommonJS 模組可以非同步匯入 ES 模組。
es2015、es2020、es2022、esnext
摘要
- 對 bundler、Bun 和 tsx 使用
esnext搭配--moduleResolution bundler。 - 請勿用於 Node.js。對 Node.js 發射 ES 模組時,請在 package.json 中使用
node16或nodenext搭配"type": "module"。 - 在非宣告檔案中不允許使用
import mod = require("mod")。 es2020新增對import.meta屬性的支援。es2022新增對頂層await的支援。esnext是個不斷變動的目標,可能包含對 ECMAScript 模組的第 3 階段提案的支援。- 發射的檔案是 ES 模組,但相依項可以是任何格式。
範例
tsimport x, { y, z } from "mod";import * as mod from "mod";const dynamic = import("mod");console.log(x, y, z, mod, dynamic);export const e1 = 0;export default "default export";
jsimport x, { y, z } from "mod";import * as mod from "mod";const dynamic = import("mod");console.log(x, y, z, mod, dynamic);export const e1 = 0;export default "default export";
commonjs
摘要
- 您可能不應該使用這個。使用
node16或nodenext為 Node.js 發射 CommonJS 模組。 - 發射的檔案是 CommonJS 模組,但相依性可能是任何格式。
- 動態
import()轉換為require()呼叫的 Promise。 esModuleInterop會影響預設和命名空間輸入的輸出程式碼。
範例
輸出顯示為
esModuleInterop: false。
tsimport x, { y, z } from "mod";import * as mod from "mod";const dynamic = import("mod");console.log(x, y, z, mod, dynamic);export const e1 = 0;export default "default export";
js"use strict";Object.defineProperty(exports, "__esModule", { value: true });exports.e1 = void 0;const mod_1 = require("mod");const mod = require("mod");const dynamic = Promise.resolve().then(() => require("mod"));console.log(mod_1.default, mod_1.y, mod_1.z, mod);exports.e1 = 0;exports.default = "default export";
tsimport mod = require("mod");console.log(mod);export = {p1: true,p2: false};
js"use strict";const mod = require("mod");console.log(mod);module.exports = {p1: true,p2: false};
system
摘要
- 設計用於與 SystemJS 模組載入器 搭配使用。
範例
tsimport x, { y, z } from "mod";import * as mod from "mod";const dynamic = import("mod");console.log(x, y, z, mod, dynamic);export const e1 = 0;export default "default export";
jsSystem.register(["mod"], function (exports_1, context_1) {"use strict";var mod_1, mod, dynamic, e1;var __moduleName = context_1 && context_1.id;return {setters: [function (mod_1_1) {mod_1 = mod_1_1;mod = mod_1_1;}],execute: function () {dynamic = context_1.import("mod");console.log(mod_1.default, mod_1.y, mod_1.z, mod, dynamic);exports_1("e1", e1 = 0);exports_1("default", "default export");}};});
amd
摘要
- 設計用於 AMD 載入器,例如 RequireJS。
- 您可能不應該使用這個。請改用打包器。
- 發出的檔案是 AMD 模組,但相依性可能是任何格式。
- 支援
outFile。
範例
tsimport x, { y, z } from "mod";import * as mod from "mod";const dynamic = import("mod");console.log(x, y, z, mod, dynamic);export const e1 = 0;export default "default export";
jsdefine(["require", "exports", "mod", "mod"], function (require, exports, mod_1, mod) {"use strict";Object.defineProperty(exports, "__esModule", { value: true });exports.e1 = void 0;const dynamic = new Promise((resolve_1, reject_1) => { require(["mod"], resolve_1, reject_1); });console.log(mod_1.default, mod_1.y, mod_1.z, mod, dynamic);exports.e1 = 0;exports.default = "default export";});
umd
摘要
- 專為 AMD 或 CommonJS 載入器設計。
- 與大多數其他 UMD 封裝不同,不會公開全域變數。
- 您可能不應該使用這個。請改用打包器。
- 發出的檔案是 UMD 模組,但相依性可能是任何格式。
範例
tsimport x, { y, z } from "mod";import * as mod from "mod";const dynamic = import("mod");console.log(x, y, z, mod, dynamic);export const e1 = 0;export default "default export";
js(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", "mod", "mod"], factory);}})(function (require, exports) {"use strict";var __syncRequire = typeof module === "object" && typeof module.exports === "object";Object.defineProperty(exports, "__esModule", { value: true });exports.e1 = void 0;const mod_1 = require("mod");const mod = require("mod");const dynamic = __syncRequire ? Promise.resolve().then(() => require("mod")) : new Promise((resolve_1, reject_1) => { require(["mod"], resolve_1, reject_1); });console.log(mod_1.default, mod_1.y, mod_1.z, mod, dynamic);exports.e1 = 0;exports.default = "default export";});
理論區段,以深入瞭解此選項的用途以及它如何融入整體編譯程序。簡而言之,moduleResolution 控制 TypeScript 如何將 模組指定項(import/export/require 陳述式中的字串文字)解析為磁碟上的檔案,而且應設定為與目標執行階段或套件管理程式所使用的模組解析器相符。
共用功能和程序
檔案副檔名替換
TypeScript 始終希望內部解析為可提供型別資訊的檔案,同時確保執行階段或套件管理程式可以使用相同的路徑解析為提供 JavaScript 實作的檔案。對於任何根據指定的 moduleResolution 演算法會觸發在執行階段或套件管理程式中查詢 JavaScript 檔案的模組指定項,TypeScript 會先嘗試尋找具有相同名稱和類似檔案副檔名的 TypeScript 實作檔案或型別宣告檔案。
| 執行階段查詢 | TypeScript 查詢 #1 | TypeScript 查詢 #2 | TypeScript 查詢 #3 | TypeScript 查詢 #4 | TypeScript 查詢 #5 |
|---|---|---|---|---|---|
/mod.js |
/mod.ts |
/mod.tsx |
/mod.d.ts |
/mod.js |
./mod.jsx |
/mod.mjs |
/mod.mts |
/mod.d.mts |
/mod.mjs |
||
/mod.cjs |
/mod.cts |
/mod.d.cts |
/mod.cjs |
請注意,此行為與匯入中編寫的實際模組規格無關。這表示即使模組規格明確使用 .js 檔案副檔名,TypeScript 仍可解析為 .ts 或 .d.ts 檔案
tsimport x from "./mod.js";// Runtime lookup: "./mod.js"// TypeScript lookup #1: "./mod.ts"// TypeScript lookup #2: "./mod.d.ts"// TypeScript lookup #3: "./mod.js"
請參閱 TypeScript 模仿主機的模組解析,但加上類型,了解 TypeScript 的模組解析為何會這樣運作。
相對檔案路徑解析
TypeScript 的所有 moduleResolution 演算法都支援透過包含檔案副檔名的相對路徑來參照模組(將根據 上述規則 進行替換)
ts// @Filename: a.tsexport {};// @Filename: b.tsimport {} from "./a.js"; // ✅ Works in every `moduleResolution`
不帶副檔名的相對路徑
在某些情況下,執行時期或套件管理程式允許從相對路徑中省略 .js 檔案副檔名。TypeScript 支援此行為,其中 moduleResolution 設定和內容指出執行時期或套件管理程式支援此行為
ts// @Filename: a.tsexport {};// @Filename: b.tsimport {} from "./a";
如果 TypeScript 判定執行時期會針對模組規格 "./a" 執行 ./a.js 的查詢,則 ./a.js 將進行 副檔名替換,並在這個範例中解析為檔案 a.ts。
在 Node.js 的 import 路徑中不支援不帶副檔名的相對路徑,且在 package.json 檔案中指定的檔案路徑也不總是支援。TypeScript 目前從不支援省略 .mjs/.mts 或 .cjs/.cts 檔案副檔名,即使某些執行時期和套件管理程式支援。
目錄模組(索引檔案解析)
在某些情況下,可以將目錄(而非檔案)作為模組來參考。在最簡單且最常見的情況下,這涉及執行階段或套件管理工具在目錄中尋找 `index.js` 檔案。TypeScript 支援此行為,其中 `moduleResolution` 設定和內容指出執行階段或套件管理工具支援此行為
ts// @Filename: dir/index.tsexport {};// @Filename: b.tsimport {} from "./dir";
如果 TypeScript 判定執行階段將針對模組規格說明 `"./dir"` 執行 `./dir/index.js` 的查詢,則 `./dir/index.js` 將進行 副檔名替換,並在本範例中解析為檔案 `dir/index.ts`。
目錄模組也可能包含 `package.json` 檔案,其中支援解析 `「main」` 和 `「types」` 欄位,且優先於 `index.js` 查詢。目錄模組中也支援 `「typesVersions」` 欄位。
請注意,目錄模組與 `node_modules` 套件 不同,且僅支援套件可用的功能子集,且在某些情況下根本不支援。Node.js 將其視為 舊版功能。
路徑
概觀
TypeScript 提供一種方式,可使用 `paths` 編譯器選項,以裸露規格說明覆寫編譯器的模組解析。雖然此功能最初設計為與 AMD 模組載入器搭配使用(一種在 ESM 存在或套件管理工具廣泛使用之前,在瀏覽器中執行模組的方法),但當執行階段或套件管理工具支援 TypeScript 未建模的模組解析功能時,它在今日仍有用途。例如,當以 `--experimental-network-imports` 執行 Node.js 時,您可以手動指定特定 `https://` 匯入的本機類型定義檔
json{"compilerOptions": {"module": "nodenext","paths": {"https://esm.sh/lodash@4.17.21": ["./node_modules/@types/lodash/index.d.ts"]}}}
ts// Typed by ./node_modules/@types/lodash/index.d.ts due to `paths` entryimport { add } from "https://esm.sh/lodash@4.17.21";
使用套件管理工具建立的應用程式通常會在套件管理工具設定中定義方便的路徑別名,然後使用 paths 告知 TypeScript 這些別名
json{"compilerOptions": {"module": "esnext","moduleResolution": "bundler","paths": {"@app/*": ["./src/*"]}}}
paths 不會影響發射
paths 選項不會變更 TypeScript 發射的程式碼中的匯入路徑。因此,很容易建立在 TypeScript 中看似可行,但在執行階段會發生錯誤的路徑別名
json{"compilerOptions": {"module": "nodenext","paths": {"node-has-no-idea-what-this-is": ["./oops.ts"]}}}
ts// TypeScript: ✅// Node.js: 💥import {} from "node-has-no-idea-what-this-is";
套件管理的應用程式可以設定 paths,但已發布的函式庫不應設定,因為發射的 JavaScript 在沒有使用者同時為 TypeScript 和套件管理工具設定相同別名的情況下,無法供函式庫的使用者使用。函式庫和應用程式都可以考慮使用 package.json "imports" 作為方便的 paths 別名的標準替代方案。
paths 不應指向單一儲存庫套件或 node_modules 套件
雖然與 paths 別名相符的模組指定符是裸指定符,但別名解析後,模組解析會在解析的路徑上以相對路徑的方式繼續進行。因此,node_modules 套件查詢中發生的解析功能,包括 package.json "exports" 欄位支援,在與 paths 別名相符時不會生效。如果 paths 用於指向 node_modules 套件,可能會導致令人驚訝的行為
ts{"compilerOptions": {"paths": {"pkg": ["./node_modules/pkg/dist/index.d.ts"],"pkg/*": ["./node_modules/pkg/*"]}}}
雖然此組態可能會模擬套件解析的部分行為,但它會覆寫套件的 package.json 檔案所定義的任何 main、types、exports 和 typesVersions,且從套件匯入的動作可能會在執行階段失敗。
單一儲存庫中相互參照的套件也適用相同的注意事項。與其使用 paths 讓 TypeScript 人工將 "@my-scope/lib" 解析為同層級套件,最好透過 npm、yarn 或 pnpm 使用工作區將套件符號連結至 node_modules,讓 TypeScript 和執行階段或套件管理工具執行實際的 node_modules 套件查詢。如果單一儲存庫套件將發佈至 npm,這點特別重要,因為套件在使用者安裝後會透過 node_modules 套件查詢相互參照,而使用工作區可讓您在本地端開發期間測試該行為。
與 baseUrl 的關係
當提供 baseUrl 時,每個 paths 陣列中的值會相對於 baseUrl 解析。否則,它們會相對於定義它們的 tsconfig.json 檔案解析。
萬用字元取代
paths 模式可以包含一個單一的 * 萬用字元,它會比對任何字串。然後可以在檔案路徑值中使用 * 符號來取代比對到的字串
json{"compilerOptions": {"paths": {"@app/*": ["./src/*"]}}}
在解析 "@app/components/Button" 的匯入時,TypeScript 會比對 @app/*,將 * 繫結至 components/Button,然後嘗試解析相對於 tsconfig.json 路徑的 ./src/components/Button 路徑。此查詢的剩餘部分會根據 moduleResolution 設定遵循與任何其他 相對路徑查詢 相同的規則。
當多個模式比對到模組規格時,會使用在任何 * 符號之前具有最長比對字首的模式
json{"compilerOptions": {"paths": {"*": ["./src/foo/one.ts"],"foo/*": ["./src/foo/two.ts"],"foo/bar": ["./src/foo/three.ts"]}}}
在解析「"foo/bar"」的匯入時,所有三個 paths 模式都符合,但最後一個會被使用,因為「"foo/bar"」比「"foo/"」和「""」長。
備用
可以為路徑對應提供多個檔案路徑。如果解析其中一個路徑失敗,將嘗試陣列中的下一個路徑,直到解析成功或到達陣列結尾。
json{"compilerOptions": {"paths": {"*": ["./vendor/*", "./types/*"]}}}
baseUrl
baseUrl設計用於與 AMD 模組載入器搭配使用。如果您沒有使用 AMD 模組載入器,您可能不應該使用baseUrl。自 TypeScript 4.1 起,baseUrl不再需要使用paths,也不應該只用於設定paths值解析的目錄。
baseUrl 編譯器選項可以與任何 moduleResolution 模式結合,並指定一個目錄,用於解析裸露規範 (不以 ./、../ 或 / 開頭的模組規範)。在支援它們的 moduleResolution 模式中,baseUrl 優先於 node_modules 套件查詢。
在執行 baseUrl 查詢時,解析會遵循與其他相對路徑解析相同的規則。例如,在支援 不帶副檔名的相對路徑 的 moduleResolution 模式中,如果 baseUrl 設定為 /src,模組規範 "some-file" 可能會解析為 /src/some-file.ts。
相對模組規範的解析永遠不會受到 baseUrl 選項影響。
node_modules 套件查詢
Node.js 將不是相對路徑、絕對路徑或 URL 的模組規範視為對套件的參考,它會在 node_modules 子目錄中查詢這些套件。套件管理員方便地採用這種行為,讓其使用者可以使用與在 Node.js 中相同的依賴關係管理系統,甚至通常使用相同的依賴關係。TypeScript 的所有 moduleResolution 選項(classic 除外)都支援 node_modules 查詢。(classic 在其他解析方式失敗時支援在 node_modules/@types 中查詢,但從不直接在 node_modules 中尋找套件。) 每次 node_modules 套件查詢都具有以下結構 (在優先權較高的裸露規範規則 (例如 paths、baseUrl、自訂名稱匯入和 package.json "imports" 查詢) 耗盡後開始)
- 對於匯入檔案的每個祖先目錄,如果其中存在
node_modules目錄- 如果
node_modules中存在與套件同名的目錄- 嘗試從套件目錄解析類型。
- 如果找到結果,則傳回結果並停止搜尋。
- 如果
node_modules/@types中存在與套件同名的目錄- 嘗試從
@types套件目錄解析類型。 - 如果找到結果,則傳回結果並停止搜尋。
- 嘗試從
- 如果
- 重複對所有
node_modules目錄進行先前的搜尋,但這次允許 JavaScript 檔案作為結果,且不搜尋@types目錄。
所有 moduleResolution 模式(除了 classic)都遵循此模式,而一旦找到,它們如何從套件目錄解析的詳細資訊則有所不同,並在以下各節中說明。
package.json "exports"
當 moduleResolution 設定為 node16、nodenext 或 bundler,且 resolvePackageJsonExports 未停用時,TypeScript 會在由 裸指定符 node_modules 套件查詢觸發的套件目錄中解析時,遵循 Node.js 的 package.json "exports" 規格。
TypeScript 透過 "exports" 解析模組指定符為檔案路徑的實作完全遵循 Node.js。然而,一旦解析出檔案路徑,TypeScript 仍會 嘗試多個檔案副檔名,以優先尋找類型。
透過 條件式 "exports" 解析時,TypeScript 會永遠比對 "types" 和 "default" 條件(如果存在的話)。此外,TypeScript 會根據 "typesVersions" 中實作的相同版本比對規則,比對格式為 "types@{selector}" 的版本化類型條件(其中 {selector} 是與 "typesVersions" 相容的版本選擇器)。其他不可設定的條件會依賴於 moduleResolution 模式,並在以下各節中說明。可以設定其他條件,以與 customConditions 編譯器選項比對。
請注意,"exports" 的存在會阻止任何未明確列出或未與 "exports" 中的樣式比對的子路徑進行解析。
範例:子路徑、條件和副檔名替換
情境:在具有以下 package.json 的套件目錄中,要求 "pkg/subpath",條件為 ["types", "node", "require"](由 moduleResolution 設定和觸發模組解析要求的內容所決定)
json{"name": "pkg","exports": {".": {"import": "./index.mjs","require": "./index.cjs"},"./subpath": {"import": "./subpath/index.mjs","require": "./subpath/index.cjs"}}}
套件目錄中的解析流程
"exports"是否存在?是。"exports"是否有"./subpath"項目?是。exports["./subpath"]中的值為物件—它必須指定條件。- 第一個條件
"import"是否與此要求相符?否。 - 第二個條件
"require"是否與此要求相符?是。 - 路徑
"./subpath/index.cjs"是否有已識別的 TypeScript 檔案副檔名?否,因此使用副檔名替換。 - 透過 副檔名替換,嘗試下列路徑,傳回第一個存在的路徑,否則傳回
undefined./subpath/index.cts./subpath/index.d.cts./subpath/index.cjs
如果 ./subpath/index.cts 或 ./subpath.d.cts 存在,則解析完成。否則,解析會根據 node_modules 套件查詢 規則,在 node_modules/@types/pkg 和其他 node_modules 目錄中搜尋類型。如果找不到類型,則會在所有 node_modules 中執行第二次傳遞,解析為 ./subpath/index.cjs(假設它存在),這算是一種成功的解析,但不會提供類型,導致輸入為 any 型別,如果啟用,還會產生 noImplicitAny 錯誤。
範例:明確的 "types" 條件
情境:在一個有以下 package.json 的套件目錄中,"pkg/subpath" 要求條件 ["types", "node", "import"](由 moduleResolution 設定和觸發模組解析要求的內容決定)
json{"name": "pkg","exports": {"./subpath": {"import": {"types": "./types/subpath/index.d.mts","default": "./es/subpath/index.mjs"},"require": {"types": "./types/subpath/index.d.cts","default": "./cjs/subpath/index.cjs"}}}}
套件目錄中的解析流程
"exports"是否存在?是。"exports"是否有"./subpath"項目?是。exports["./subpath"]中的值為物件—它必須指定條件。- 第一個條件
"import"符合這個要求嗎?是。 exports["./subpath"].import的值是一個物件—它必須指定條件。- 第一個條件
"types"符合這個要求嗎?是。 - 路徑
"./types/subpath/index.d.mts"有已識別的 TypeScript 檔案副檔名嗎?是,所以不要使用副檔名替換。 - 如果檔案存在,傳回路徑
"./types/subpath/index.d.mts",否則傳回undefined。
範例:已設定版本的 "types" 條件
情境:使用 TypeScript 4.7.5,在一個有以下 package.json 的套件目錄中,"pkg/subpath" 要求條件 ["types", "node", "import"](由 moduleResolution 設定和觸發模組解析要求的內容決定)
json{"name": "pkg","exports": {"./subpath": {"types@>=5.2": "./ts5.2/subpath/index.d.ts","types@>=4.6": "./ts4.6/subpath/index.d.ts","types": "./tsold/subpath/index.d.ts","default": "./dist/subpath/index.js"}}}
套件目錄中的解析流程
"exports"是否存在?是。"exports"是否有"./subpath"項目?是。exports["./subpath"]中的值為物件—它必須指定條件。- 第一個條件
"types@>=5.2"符合這個請求嗎?否,4.7.5 不大於或等於 5.2。 - 第二個條件
"types@>=4.6"符合這個請求嗎?是,4.7.5 大於或等於 4.6。 - 路徑
"./ts4.6/subpath/index.d.ts"有已識別的 TypeScript 檔案副檔名嗎?是,所以不要使用副檔名替換。 - 如果檔案存在,傳回路徑
"./ts4.6/subpath/index.d.ts",否則傳回undefined。
範例:子路徑模式
情境:"pkg/wildcard.js" 請求具有條件 ["types", "node", "import"](由 moduleResolution 設定和觸發模組解析請求的內容所決定),且位於具有下列 package.json 的套件目錄中
json{"name": "pkg","type": "module","exports": {"./*.js": {"types": "./types/*.d.ts","default": "./dist/*.js"}}}
套件目錄中的解析流程
"exports"是否存在?是。"exports"有"./wildcard.js"項目嗎?否。- 任何具有
*的金鑰是否與"./wildcard.js"相符?是,"./*.js"相符,並將wildcard設定為替換。 exports["./*.js"]的值是一個物件—它必須指定條件。- 第一個條件
"types"符合這個要求嗎?是。 - 在
./types/*.d.ts中,將*替換為替換wildcard。./types/wildcard.d.ts - 路徑
"./types/wildcard.d.ts"有已識別的 TypeScript 檔案副檔名嗎?是,所以不要使用副檔名替換。 - 如果檔案存在,傳回路徑
"./types/wildcard.d.ts",否則傳回undefined。
範例:"exports" 阻擋其他子路徑
情境:"pkg/dist/index.js" 在具有下列 package.json 的套件目錄中請求
json{"name": "pkg","main": "./dist/index.js","exports": "./dist/index.js"}
套件目錄中的解析流程
"exports"是否存在?是。exports的值是一個字串—它必須是套件根目錄(".")的檔案路徑。- 請求
"pkg/dist/index.js"是針對套件根目錄嗎?否,它有子路徑dist/index.js。 - 解析失敗;傳回
undefined。
沒有 "exports",請求可能會成功,但 "exports" 的存在會阻止解析任何無法透過 "exports" 相符的子路徑。
package.json "typesVersions"
一個 node_modules 套件 或 目錄模組 可以在其 package.json 中指定 "typesVersions" 欄位,以根據 TypeScript 編譯器版本重新導向 TypeScript 的解析程序,而對於 node_modules 套件,則根據正在解析的子路徑重新導向。這允許套件作者在一個類型定義集中包含新的 TypeScript 語法,同時提供另一組定義以向後相容於較舊的 TypeScript 版本(透過 downlevel-dts 等工具)。所有 moduleResolution 模式都支援 "typesVersions";但是,在讀取 package.json "exports" 時不會讀取該欄位。
範例:將所有要求重新導向至子目錄
情境:模組使用 TypeScript 5.2 匯入 "pkg",其中 node_modules/pkg/package.json 為
json{"name": "pkg","version": "1.0.0","types": "./index.d.ts","typesVersions": {">=3.1": {"*": ["ts3.1/*"]}}}
解析程序
- (根據編譯器選項)是否存在
"exports"?否。 - 是否存在
"typesVersions"?是。 - TypeScript 版本是否
>=3.1?是。記住對應"*": ["ts3.1/*"]。 - 我們是否在套件名稱之後解析子路徑?否,僅根目錄
"pkg"。 - 是否存在
"types"?是。 "typesVersions"中的任何金鑰是否與./index.d.ts相符?是,"*"相符,並將index.d.ts設定為替換。- 在
ts3.1/*中,將*取代為替換./index.d.ts:ts3.1/index.d.ts。 - 路徑
./ts3.1/index.d.ts是否有已識別的 TypeScript 檔案副檔名?是,因此不要使用副檔名替換。 - 如果檔案存在,則傳回路徑
./ts3.1/index.d.ts,否則傳回undefined。
範例:重新導向特定檔案的要求
情境:模組使用 TypeScript 3.9 匯入 "pkg",其中 node_modules/pkg/package.json 為
json{"name": "pkg","version": "1.0.0","types": "./index.d.ts","typesVersions": {"<4.0": { "index.d.ts": ["index.v3.d.ts"] }}}
解析程序
- (根據編譯器選項)是否存在
"exports"?否。 - 是否存在
"typesVersions"?是。 - TypeScript 版本是否
<4.0?是。記住對應"index.d.ts - 我們是否在套件名稱之後解析子路徑?否,僅根目錄
"pkg"。 - 是否存在
"types"?是。 "typesVersions"中的任何鍵與./index.d.ts相符嗎?是的,"index.d.ts"相符。- 路徑
./index.v3.d.ts是否有已識別的 TypeScript 檔案副檔名?是的,因此不要使用副檔名替換。 - 如果檔案存在,傳回路徑
./index.v3.d.ts,否則傳回undefined。
package.json "main" 和 "types"
如果目錄的 package.json "exports" 欄位未讀取(可能是因為編譯器選項,或因為它不存在,或因為目錄正在解析為 目錄模組,而不是 node_modules 套件),且模組指定符在套件名稱或包含 package.json 的目錄之後沒有子路徑,TypeScript 會嘗試從這些 package.json 欄位解析,依序嘗試,以找到套件或目錄的主模組
"types""typings"(舊版)"main"
假設在 "types" 找到的宣告檔精確表示在 "main" 找到的實作檔。如果 "types" 和 "typings" 不存在或無法解析,TypeScript 會讀取 "main" 欄位並執行 副檔名替換 以找到宣告檔。
在將已輸入的套件發佈到 npm 時,建議包含 "types" 欄位,即使 副檔名替換 或 package.json "exports" 使其不必要,因為只有當 package.json 包含 "types" 欄位時,npm 才會在套件登錄清單上顯示 TS 圖示。
與套件相關的檔案路徑
如果 package.json "exports" 或 package.json "typesVersions" 都不適用,則會根據適用的 相對路徑 解析規則,解析裸套件指定項子的子路徑相對於套件目錄。在尊重 [package.json "exports"] 的模式中,即使無法透過 "exports" 解析匯入,此行為也會因套件的 package.json 中存在 "exports" 欄位而遭到阻擋,如 上方的範例 所示。另一方面,如果無法透過 "typesVersions" 解析匯入,則會嘗試套件相對檔案路徑解析作為後備方案。
當支援套件相對路徑時,它們會根據考慮 moduleResolution 模式和內容的任何其他相對路徑的相同規則進行解析。例如,在 --moduleResolution nodenext 中,目錄模組 和 無副檔名路徑 僅在 require 呼叫中受支援,而不在 import 中受支援。
ts// @Filename: module.mtsimport "pkg/dist/foo"; // ❌ import, needs `.js` extensionimport "pkg/dist/foo.js"; // ✅import foo = require("pkg/dist/foo"); // ✅ require, no extension needed
package.json "imports" 和自訂名稱匯入
當 moduleResolution 設為 node16、nodenext 或 bundler,且 resolvePackageJsonImports 未停用時,TypeScript 會嘗試透過匯入檔案最近的祖先 package.json 的 "imports" 欄位,解析以 # 開頭的匯入路徑。類似地,當 package.json "exports" 查詢 已啟用時,TypeScript 會嘗試透過該 package.json 的 "exports" 欄位,解析以目前套件名稱開頭的匯入路徑,也就是匯入檔案最近的祖先 package.json 的 "name" 欄位中的值。這兩個功能都允許套件中的檔案匯入同一個套件中的其他檔案,取代相對匯入路徑。
TypeScript 遵循 Node.js 的解析演算法,用於 "imports" 和 自我參照,直到檔案路徑解析完畢。在那個時候,TypeScript 的解析演算法會根據包含要解析的 "imports" 或 "exports" 的 package.json 是否屬於 node_modules 相依性或正在編譯的本機專案(也就是說,其目錄包含該專案的 tsconfig.json 檔案,其中包含匯入檔案)而分岔。
- 如果 package.json 在
node_modules中,如果檔案路徑還沒有已識別的 TypeScript 檔案副檔名,TypeScript 會對檔案路徑套用 副檔名替換,並檢查結果檔案路徑是否存在。 - 如果 package.json 是本機專案的一部分,會執行額外的重新對應步驟,以尋找最終會產生從
"imports"解析而來的輸出 JavaScript 或宣告檔案路徑的輸入 TypeScript 實作檔案。沒有這個步驟,任何解析"imports"路徑的編譯都會參照前一次編譯的輸出檔案,而不是其他打算包含在目前編譯中的輸入檔案。這個重新對應會使用 tsconfig.json 中的outDir/declarationDir和rootDir,因此使用"imports"通常需要設定明確的rootDir。
這個變異允許套件作者撰寫僅參照將發佈到 npm 的編譯輸出的 "imports" 和 "exports" 欄位,同時仍然允許本機開發使用原始 TypeScript 原始檔。
範例:具有條件的本機專案
場景:"/src/main.mts" 匯入 "#utils",條件為 ["types", "node", "import"](由 moduleResolution 設定和觸發模組解析要求的內容所決定),在具有 tsconfig.json 和 package.json 的專案目錄中
json// tsconfig.json{"compilerOptions": {"moduleResolution": "node16","resolvePackageJsonImports": true,"rootDir": "./src","outDir": "./dist"}}
json// package.json{"name": "pkg","imports": {"#utils": {"import": "./dist/utils.d.mts","require": "./dist/utils.d.cts"}}}
解析程序
- 匯入路徑以
#開頭,嘗試透過"imports"解析。 - 最近的祖先 package.json 中是否存在
"imports"?是。 "imports"物件中是否存在"#utils"?是。imports["#utils"]中的值是一個物件—它必須指定條件。- 第一個條件
"import"符合這個要求嗎?是。 - 我們是否應嘗試將輸出路徑對應到輸入路徑?是,因為:
- package.json 是否在
node_modules中?否,它在本機專案中。 - tsconfig.json 是否在 package.json 目錄中?是。
- package.json 是否在
- 在
./dist/utils.d.mts中,將outDir前綴替換為rootDir。./src/utils.d.mts - 將輸出副檔名
.d.mts替換為對應的輸入副檔名.mts。./src/utils.mts - 如果檔案存在,傳回路徑
"./src/utils.mts"。 - 否則,如果檔案存在,傳回路徑
"./dist/utils.d.mts"。
範例:node_modules 相依性與子路徑模式
情境:"/node_modules/pkg/main.mts" 匯入 "#internal/utils",條件為 ["types", "node", "import"](由 moduleResolution 設定和觸發模組解析要求的內容所決定),其 package.json 如下:
json// /node_modules/pkg/package.json{"name": "pkg","imports": {"#internal/*": {"import": "./dist/internal/*.mjs","require": "./dist/internal/*.cjs"}}}
解析程序
- 匯入路徑以
#開頭,嘗試透過"imports"解析。 - 最近的祖先 package.json 中是否存在
"imports"?是。 "#internal/utils"是否存在於"imports"物件中?否,檢查模式是否相符。- 是否有任何包含
*的金鑰與"#internal/utils"相符?是,"#internal/*"相符,並將utils設為替換。 imports["#internal/*"]的值是一個物件,它必須指定條件。- 第一個條件
"import"符合這個要求嗎?是。 - 我們是否應嘗試將輸出路徑對應到輸入路徑?否,因為 package.json 在
node_modules中。 - 在
./dist/internal/*.mjs中,將*替換為替換utils。./dist/internal/utils.mjs - 路徑
./dist/internal/utils.mjs是否具有已識別的 TypeScript 檔案副檔名?否,嘗試副檔名替換。 - 透過 副檔名替換,嘗試下列路徑,傳回第一個存在的路徑,否則傳回
undefined./dist/internal/utils.mts./dist/internal/utils.d.mts./dist/internal/utils.mjs
node16、nodenext
這些模式反映 Node.js v12 及更新版本的模組解析行為。(node16 和 nodenext 目前相同,但如果 Node.js 在未來對其模組系統進行重大變更,node16 將會凍結,而 nodenext 將會更新以反映新的行為。)在 Node.js 中,ECMAScript 匯入的解析演算法與 CommonJS require 呼叫的演算法有顯著不同。對於每個要解析的模組識別碼,語法和 匯入檔案的模組格式 會先用來判斷模組識別碼在發出的 JavaScript 中會是 import 還是 require。然後將該資訊傳遞給模組解析器,以確定要使用哪個解析演算法(以及是否要對 package.json "exports" 或 "imports" 使用 "import" 或 "require" 條件)。
預設情況下,判斷為 CommonJS 格式 的 TypeScript 檔案仍可以使用
import和export語法,但發出的 JavaScript 會改用require和module.exports。這表示通常會看到使用require演算法解析的import陳述式。如果這會造成混淆,可以啟用verbatimModuleSyntax編譯器選項,這會禁止使用會發出為require呼叫的import陳述式。
請注意,根據 Node.js 的行為,動態 import() 呼叫總是使用 import 演算法解析。但是,import() 類型會根據匯入檔案的格式解析(為了與現有的 CommonJS 格式類型宣告保持向後相容性)
ts// @Filename: module.mtsimport x from "./mod.js"; // `import` algorithm due to file format (emitted as-written)import("./mod.js"); // `import` algorithm due to syntax (emitted as-written)type Mod = typeof import("./mod.js"); // `import` algorithm due to file formatimport mod = require("./mod"); // `require` algorithm due to syntax (emitted as `require`)// @Filename: commonjs.ctsimport x from "./mod"; // `require` algorithm due to file format (emitted as `require`)import("./mod.js"); // `import` algorithm due to syntax (emitted as-written)type Mod = typeof import("./mod"); // `require` algorithm due to file formatimport mod = require("./mod"); // `require` algorithm due to syntax (emitted as `require`)
暗示和強制選項
--moduleResolution node16和nodenext必須與其 對應的module值 配對。
支援的功能
功能依優先順序列出。
匯入 |
需要 |
|
|---|---|---|
路徑 |
✅ | ✅ |
基本網址 |
✅ | ✅ |
node_modules 套件查詢 |
✅ | ✅ |
package.json "exports" |
✅ 符合 types、node、import |
✅ 符合 types、node、require |
package.json "imports" 和自訂名稱匯入 |
✅ 符合 types、node、import |
✅ 符合 types、node、require |
package.json "typesVersions" |
✅ | ✅ |
| 套件相關路徑 | ✅ 當 exports 不存在時 |
✅ 當 exports 不存在時 |
| 完整相對路徑 | ✅ | ✅ |
| 無副檔名的相對路徑 | ❌ | ✅ |
| 目錄模組 | ❌ | ✅ |
bundler
--moduleResolution bundler 嘗試模擬大多數 JavaScript 捆綁器共有的模組解析行為。簡而言之,這表示支援所有傳統上與 Node.js 的 CommonJS require 解析演算法相關的行為,例如 node_modules 查詢、目錄模組 和 無副檔名路徑,同時也支援較新的 Node.js 解析功能,例如 package.json "exports" 和 package.json "imports"。
這與 node16 和 nodenext 在 CommonJS 模式中解析的行為非常相似,但在 bundler 中,用於解析 package.json "exports" 和 "imports" 的條件始終是 "types" 和 "import"。為了了解原因,讓我們比較一下 nodenext 中 .ts 檔案中的匯入會發生什麼情況
ts// index.tsimport { foo } from "pkg";
在 --module nodenext --moduleResolution nodenext 中,--module 設定會先 判定 匯入會以 import 或 require 呼叫的形式發送到 .js 檔案中,並將該資訊傳遞給 TypeScript 的模組解析器,解析器會決定相應地符合 "import" 或 "require" 條件。這可確保 TypeScript 的模組解析程序(儘管從輸入的 .ts 檔案中執行)會反映 Node.js 的模組解析程序在執行輸出的 .js 檔案時會發生什麼情況。
另一方面,當使用捆綁器時,捆綁器通常會直接處理原始的 .ts 檔案,並對未轉換的 import 陳述式執行其模組解析程序。在此情況下,思考 TypeScript 將如何發送 import 沒有什麼意義,因為 TypeScript 根本沒有用於發送任何內容。就捆綁器而言,import 是 import,require 是 require,因此用於解析 package.json "exports" 和 "imports" 的條件是由輸入的 .ts 檔案中看到的語法決定的。同樣地,TypeScript 的模組解析程序在 --moduleResolution bundler 中使用的條件也由輸入 TypeScript 檔案中的輸入語法決定,只是目前完全不會解析 require 呼叫
ts// Some library file:declare function require(module: string): any;// index.tsimport { foo } from "pkg"; // Resolved with "import" conditionimport pkg2 = require("pkg"); // Not allowedconst pkg = require("pkg"); // Not an error, but not resolved to anything// ^? any
由於 TypeScript 目前不支援在 --moduleResolution bundler 中解析 require 呼叫,因此它實際上解析的所有內容都會使用 "import" 條件。
隱含和強制選項
--moduleResolution bundler必須與--module esnext選項配對使用。--moduleResolution bundler暗示--allowSyntheticDefaultImports。
支援的功能
路徑✅基本網址✅node_modules套件查詢 ✅- package.json
"exports"✅ 符合types、import - package.json
"imports"和自訂名稱匯入 ✅ 符合types、import - package.json
"typesVersions"✅ - 套件相關路徑 ✅ 當
exports不存在時 - 完整相對路徑 ✅
- 無副檔名的相對路徑 ✅
- 目錄模組 ✅
node10(以前稱為 node)
--moduleResolution node 已在 TypeScript 5.0 中重新命名為 node10(保留 node 作為向後相容性的別名)。它反映了 Node.js 早於 v12 版本中存在的 CommonJS 模組解析演算法。它不應再使用。
支援的功能
路徑✅基本網址✅node_modules套件查詢 ✅- package.json
"exports"❌ - package.json
"imports"和自訂名稱匯入 ❌ - package.json
"typesVersions"✅ - 套件相關路徑 ✅
- 完整相對路徑 ✅
- 無副檔名的相對路徑 ✅
- 目錄模組 ✅
classic
請勿使用 classic。