模組語法
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 中不存在,因此只能在非發射位置(non-emitting positions)使用它們。
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 檔案可以使用 JavaScript 語法 module.exports = ... 和 const mod = require("...") 的直接模擬。
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
環境模組
TypeScript 支援在指令碼(非模組)檔案中使用語法來宣告存在於執行時但沒有對應檔案的模組。這些環境模組通常代表執行時提供的模組,例如 Node.js 中的 "fs" 或 "path"。
tsdeclare module "path" {export function normalize(p: string): string;export function join(...paths: any[]): string;export var sep: string;}
一旦環境模組載入到 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";
環境模組宣告很容易與 模組擴充套件 (module augmentations) 混淆,因為它們使用相同的語法。當檔案是模組時(意味著它有頂層的 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;}
環境模組可以在模組宣告體內部使用匯入來引用其他模組,而無需將包含該宣告的檔案變為模組(否則會將該環境模組宣告變為模組擴充套件)。
tsdeclare module "m" {// Moving this outside "m" would totally change the meaning of the file!import { SomeType } from "other";export function f(): SomeType;}
模式環境模組在其名稱中包含單個 * 萬用字元,用於匹配匯入路徑中的零個或多個字元。這對於宣告自定義載入器提供的模組非常有用。
tsdeclare module "*.html" {const content: string;export default content;}
module 編譯器選項
本節討論每個 module 編譯器選項值的詳細資訊。有關該選項及其在整個編譯過程中的定位的更多背景資訊,請參閱 模組輸出格式 理論部分。簡而言之,module 編譯器選項歷史上僅用於控制生成的 JavaScript 檔案的輸出模組格式。然而,最近新增的 node16、node18 和 nodenext 值描述了 Node.js 模組系統的廣泛特性,包括支援哪些模組格式、如何確定每個檔案的模組格式,以及不同模組格式如何互操作。
node16, node18, node20, nodenext
Node.js 同時支援 CommonJS 和 ECMAScript 模組,並且有針對每個檔案可以使用哪種格式以及兩種格式如何互操作的特定規則。node16、node18 和 nodenext 描述了 Node.js 雙格式模組系統的全部行為,並以 CommonJS 或 ESM 格式生成檔案。這與所有其他 module 選項不同,其他選項是執行時無關的,並將所有輸出檔案強制轉換為單一格式,由使用者確保輸出對他們的執行時是有效的。
一個常見的誤解是
node16—nodenext只生成 ES 模組。實際上,這些模式描述了支援 ES 模組的 Node.js 版本,而不僅僅是使用 ES 模組的專案。根據每個檔案的 檢測到的模組格式,支援生成 ESM 和 CommonJS。因為它們是唯一反映 Node.js 雙模組系統複雜性的module選項,所以它們是所有打算在 Node.js v12 或更高版本中執行的應用程式和庫的唯一正確module選項,無論它們是否使用 ES 模組。
固定版本的 node16 和 node18 模式代表了在其各自 Node.js 版本中穩定的模組系統行為,而 nodenext 模式會隨最新的 Node.js 穩定版本而變化。下表總結了這三種模式當前的區別
target |
moduleResolution |
import 斷言 (import assertions) | import 屬性 (import attributes) | JSON 匯入 | require(esm) | |
|---|---|---|---|---|---|---|
| node16 | es2022 |
node16 |
❌ | ❌ | 無限制 | ❌ |
| node18 | es2022 |
node16 |
✅ | ✅ | 需要 type "json" |
❌ |
| nodenext | esnext |
nodenext |
❌ | ✅ | 需要 type "json" |
✅ |
模組格式檢測
.mts/.mjs/.d.mts檔案始終為 ES 模組。.cts/.cjs/.d.cts檔案始終為 CommonJS 模組。- 如果最近的祖先 package.json 檔案包含
"type": "module",則.ts/.tsx/.js/.jsx/.d.ts檔案為 ES 模組,否則為 CommonJS 模組。
輸入 .ts/.tsx/.mts/.cts 檔案的檢測到的模組格式決定了所生成的 JavaScript 檔案的模組格式。因此,例如,一個完全由 .ts 檔案組成的專案在 --module nodenext 下預設會全部生成為 CommonJS 模組,並且可以透過在專案 package.json 中新增 "type": "module" 來強制全部生成為 ES 模組。
互操作規則
- 當 ES 模組引用 CommonJS 模組時:
- 當 CommonJS 模組引用 ES 模組時:
- 在
node16和node18中,require不能引用 ES 模組。對於 TypeScript,這包括被 檢測 為 CommonJS 模組的檔案中的import語句,因為這些import語句在生成的 JavaScript 中將被轉換為require呼叫。 - 在
nodenext中,為反映 Node.js v22.12.0 及更高版本的行為,require可以引用 ES 模組。在 Node.js 中,如果 ES 模組或其任何匯入的模組使用頂層await,則會丟擲錯誤。TypeScript 不會嘗試檢測這種情況,也不會發出編譯時錯誤。require呼叫的結果是模組的模組名稱空間物件(Module Namespace Object),即與同一模組的await import()結果相同(但無需使用await`)。 - 動態
import()呼叫始終可用於匯入 ES 模組。它返回一個包含模組的模組名稱空間物件的 Promise(與從另一個 ES 模組執行import * as ns from "./module.js"得到的結果相同)。
- 在
生成 (Emit)
每個檔案的生成格式由每個檔案的 檢測到的模組格式 決定。ESM 生成類似於 --module esnext,但對 import x = require("...") 有特殊的轉換,這在 --module esnext 中是不允許的。
ts// @Filename: main.tsimport x = require("mod");
js// @Filename: main.jsimport { createRequire as _createRequire } from "module";const __require = _createRequire(import.meta.url);const x = __require("mod");
CommonJS 生成類似於 --module commonjs,但動態 import() 呼叫不會被轉換。此處顯示的生成是在啟用了 esModuleInterop 的情況下。
ts// @Filename: main.tsimport fs from "fs"; // transformedconst dynamic = import("mod"); // not transformed
js// @Filename: main.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隱含並強制執行--moduleResolution nodenext。--module node18或node16隱含並強制執行--moduleResolution node16。--module nodenext隱含--target esnext。--module node18或node16隱含--target es2022。--module nodenext或node18或node16隱含--esModuleInterop。
總結
node16、node18和nodenext是所有打算在 Node.js v12 或更高版本中執行的應用程式和庫的唯一正確module選項,無論它們是否使用 ES 模組。node16、node18和nodenext根據每個檔案的 檢測到的模組格式,以 CommonJS 或 ESM 格式生成檔案。- Node.js 在 ESM 和 CJS 之間的互操作規則已反映在型別檢查中。
- ESM 生成將
import x = require("...")轉換為由createRequire匯入構造的require呼叫。 - CommonJS 生成不對動態
import()呼叫進行轉換,因此 CommonJS 模組可以非同步匯入 ES 模組。
preserve
在 --module preserve(在 TypeScript 5.4 中 新增)中,輸入檔案中編寫的 ECMAScript 匯入和匯出在輸出中被保留,而 CommonJS 風格的 import x = require("...") 和 export = ... 語句則生成為 CommonJS 的 require 和 module.exports。換句話說,每個單獨的匯入或匯出語句的格式都被保留,而不是被強制轉換成整個編譯(甚至整個檔案)的單一格式。
雖然在同一個檔案中混合使用 import 和 require 呼叫很少見,但這種 module 模式最能反映大多數現代打包工具 (bundler) 以及 Bun 執行時的能力。
為什麼要關心 TypeScript 在使用打包工具或 Bun 時的
module生成(您可能也設定了noEmit`)?TypeScript 的型別檢查和模組解析行為會受到它將會生成的模組格式的影響。設定module可以讓 TypeScript 瞭解您的打包工具或執行時將如何處理匯入和匯出,從而確保您在匯入值上看到的型別準確反映了執行時或打包後將發生的情況。更多討論請參閱--moduleResolution bundler。
示例
ts// @Filename: main.tsimport x, { y, z } from "mod";import mod = require("mod");const dynamic = import("mod");export const e1 = 0;export default "default export";
js// @Filename: main.jsimport x, { y, z } from "mod";const mod = require("mod");const dynamic = import("mod");export const e1 = 0;export default "default export";
隱含和強制執行的選項
--module preserve隱含--moduleResolution bundler。--module preserve隱含--esModuleInterop。
選項
--esModuleInterop僅在--module preserve中預設啟用,用於其 型別檢查 行為。由於匯入在--module preserve中永遠不會轉換為 require 呼叫,因此--esModuleInterop不會影響生成的 JavaScript。
es2015, es2020, es2022, esnext
總結
- 對於打包工具、Bun 和 tsx,請使用
esnext配合--moduleResolution bundler。 - 請勿在 Node.js 中使用。要在 Node.js 中生成 ES 模組,請使用
node16、node18或nodenext並配合 package.json 中的"type": "module"。 import mod = require("mod")在非宣告檔案中是不允許的。es2020增加了對import.meta屬性的支援。es2022增加了對頂層await的支援。esnext是一個不斷變化的目標,可能包含對 ECMAScript 模組 Stage 3 提案的支援。- 生成的檔案是 ES 模組,但依賴項可以是任何格式。
示例
ts// @Filename: main.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// @Filename: main.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、node18或nodenext為 Node.js 生成 CommonJS 模組。 - 生成的檔案是 CommonJS 模組,但依賴項可以是任何格式。
- 動態
import()被轉換為require()呼叫的 Promise。 esModuleInterop會影響預設匯入和名稱空間匯入的輸出程式碼。
示例
輸出是在啟用
esModuleInterop: false的情況下顯示的。
ts// @Filename: main.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// @Filename: main.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";
ts// @Filename: main.tsimport mod = require("mod");console.log(mod);export = {p1: true,p2: false};
js// @Filename: main.js"use strict";const mod = require("mod");console.log(mod);module.exports = {p1: true,p2: false};
system
總結
- 專為配合 SystemJS 模組載入器 使用而設計。
示例
ts// @Filename: main.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// @Filename: main.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
總結
- 專為 RequireJS 等 AMD 載入器設計。
- 您可能不應該使用此選項。請改用打包工具。
- 生成的檔案是 AMD 模組,但依賴項可以是任何格式。
- 支援
outFile。
示例
ts// @Filename: main.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// @Filename: main.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 模組,但依賴項可以是任何格式。
示例
ts// @Filename: main.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// @Filename: main.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 編譯器選項
本節介紹了多個 moduleResolution 模式共享的模組解析特性和過程,然後詳細說明了每種模式。有關該選項及其在整個編譯過程中的定位的更多背景資訊,請參閱 模組解析 理論部分。簡而言之,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 |
請注意,此行為獨立於匯入中編寫的實際模組說明符。這意味著 TypeScript 可以解析為 .ts 或 .d.ts 檔案,即使模組說明符明確使用了 .js 副檔名。
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 將其視為 遺留功能。
paths
概述
TypeScript 提供了一種使用 paths 編譯器選項覆蓋編譯器對裸說明符(bare specifiers)模組解析的方法。雖然該特性最初設計用於 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 不應指向 monorepo 包或 node_modules 包
雖然匹配 paths 別名的模組說明符是裸說明符,但一旦解析了別名,模組解析就會將解析後的路徑視為相對路徑進行處理。因此,當匹配 paths 別名時,針對 node_modules 包查詢 的解析特性(包括 package.json "exports" 欄位支援)將不會生效。如果使用 paths 指向 node_modules 包,這可能導致令人驚訝的行為。
ts{"compilerOptions": {"paths": {"pkg": ["./node_modules/pkg/dist/index.d.ts"],"pkg/*": ["./node_modules/pkg/*"]}}}
雖然此配置可能模擬包解析的部分行為,但它會覆蓋包的 package.json 檔案中定義的任何 main、types、exports 和 typesVersions,並且從該包的匯入可能會在執行時失敗。
同樣的警告也適用於在 monorepo 中相互引用的包。不要使用 paths 來使 TypeScript 人工將 "@my-scope/lib" 解析為同級包,最好透過 npm、yarn 或 pnpm 使用工作區 (workspaces) 將您的包軟連結到 node_modules 中,以便 TypeScript 和執行時或打包工具都執行真正的 node_modules 包查詢。如果 monorepo 包將釋出到 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/" 和 "" 更長。
回退 (Fallbacks)
可以為路徑對映提供多個檔案路徑。如果一個路徑的解析失敗,將嘗試陣列中的下一個路徑,直到解析成功或到達陣列末尾。
json{"compilerOptions": {"paths": {"*": ["./vendor/*", "./types/*"]}}}
baseUrl
baseUrl是為配合 AMD 模組載入器使用而設計的。如果您不使用 AMD 模組載入器,您可能不應該使用baseUrl。自 TypeScript 4.1 起,使用paths不再需要baseUrl,並且不應僅用於設定解析paths值的目錄。
baseUrl 編譯器選項可以與任何 moduleResolution 模式結合使用,並指定從其解析裸說明符(不以 ./、../ 或 / 開頭的模組說明符)的目錄。在支援 node_modules 包查詢 的 moduleResolution 模式中,baseUrl 具有更高的優先順序。
在執行 baseUrl 查詢時,解析按與其他相對路徑解析相同的規則進行。例如,在支援 無副檔名相對路徑 的 moduleResolution 模式中,如果 baseUrl 設定為 /src,模組說明符 "some-file" 可能會解析為 /src/some-file.ts。
相對模組說明符的解析永遠不受 baseUrl 選項的影響。
node_modules 包查詢
Node.js 將不是相對路徑、絕對路徑或 URL 的模組說明符視為對在 node_modules 子目錄中查詢的包的引用。打包工具方便地採用了這種行為,以允許其使用者使用與在 Node.js 中相同的依賴管理系統,甚至通常是相同的依賴項。除 classic 外,所有 TypeScript 的 moduleResolution 選項都支援 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" 中匹配模式的子路徑。
示例:子路徑、條件和副檔名替換
場景:請求 "pkg/subpath",條件為 ["types", "node", "require"](由 moduleResolution 設定和觸發模組解析請求的上下文確定),在一個包含以下 package.json 的包目錄中:
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" 條件
場景:請求 "pkg/subpath",條件為 ["types", "node", "import"](由 moduleResolution 設定和觸發模組解析請求的上下文確定),在一個包含以下 package.json 的包目錄中:
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,請求 "pkg/subpath",條件為 ["types", "node", "import"](由 moduleResolution 設定和觸發模組解析請求的上下文確定),在一個包含以下 package.json 的包目錄中:
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" 阻止其他子路徑
場景:在一個包含以下 package.json 的包目錄中請求 "pkg/dist/index.js":
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": ["index.v3.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"] 的模式中,此行為會被包的 package.json 中僅僅存在的 "exports" 欄位所阻止,即使匯入未能透過 "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" 和自引用匯入 (self-name 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 原始檔。
示例:帶有條件的本地專案
場景:在擁有 tsconfig.json 和 package.json 的專案目錄中,"/src/main.mts" 匯入了帶有條件 ["types", "node", "import"] 的 "#utils"(由 moduleResolution 設定和觸發模組解析請求的上下文確定)。
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" 匯入了帶有條件 ["types", "node", "import"](由 moduleResolution 設定和觸發模組解析請求的上下文確定)的 "#internal/utils",以及該 package.json。
json// /node_modules/pkg/package.json{"name": "pkg","imports": {"#internal/*": {"import": "./dist/internal/*.mjs","require": "./dist/internal/*.cjs"}}}
解析過程:
- 匯入路徑以
#開頭,嘗試透過"imports"進行解析。 - 最近的祖先 package.json 中是否存在
"imports"?是。 "imports"物件中是否存在"#internal/utils"?否,檢查模式匹配。- 是否有任何帶有
*的鍵與"#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 node16,node18,node20或nodenext配對使用。
支援的特性
特性按優先順序順序列出。
import |
require |
|
|---|---|---|
paths |
✅ | ✅ |
baseUrl |
✅ | ✅ |
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"。
思考 --moduleResolution bundler 和 --moduleResolution nodenext 之間的異同是很有啟發性的,特別是在它們如何決定在解析 package.json "exports" 或 "imports" 時使用哪些條件方面。考慮 .ts 檔案中的一條 import 語句。
ts// index.tsimport { foo } from "pkg";
回想一下,在 --module nodenext --moduleResolution nodenext 中,--module 設定首先 確定 匯入是作為 import 還是 require 呼叫輸出到 .js 檔案中,然後將該資訊傳遞給 TypeScript 的模組解析器,模組解析器決定相應地在 "pkg" 的 package.json "exports" 中匹配 "import" 或 "require" 條件。假設該檔案範圍內沒有 package.json。副檔名是 .ts,所以輸出副檔名將是 .js,Node.js 會將其解釋為 CommonJS,因此 TypeScript 會將此 import 輸出為 require 呼叫。所以,模組解析器在從 "pkg" 解析 "exports" 時將使用 require 條件。
--moduleResolution bundler 中發生了同樣的過程,但決定為此 import 語句輸出 import 還是 require 呼叫的規則會有所不同,因為 --moduleResolution bundler 需要使用 --module esnext 或 --module preserve。在這些模式中,ESM import 宣告始終輸出為 ESM import 宣告,因此 TypeScript 的模組解析器將接收該資訊,並在從 "pkg" 解析 "exports" 時使用 "import" 條件。
這個解釋可能有些不直觀,因為 --moduleResolution bundler 通常與 --noEmit 結合使用——打包工具通常處理原始的 .ts 檔案,並對未轉換的 import 或 require 執行模組解析。然而,為了保持一致性,TypeScript 仍然使用由 module 決定的假設輸出來通知模組解析和型別檢查。這使得 --module preserve 成為當執行時或打包工具在原始 .ts 檔案上執行時最佳的選擇,因為它意味著不進行轉換。在 --module preserve --moduleResolution bundler 下,你可以編寫會在同一個檔案中分別使用 import 和 require 條件解析的匯入和 require。
ts// index.tsimport pkg1 from "pkg"; // Resolved with "import" conditionimport pkg2 = require("pkg"); // Resolved with "require" condition
隱含和強制選項
--moduleResolution bundler必須與--module esnext或--module preserve配對使用。--moduleResolution bundler隱含--allowSyntheticDefaultImports。
支援的特性
paths✅baseUrl✅node_modules包查詢 ✅- package.json
"exports"✅ 根據語法匹配types,import/require - package.json
"imports"和自引用匯入 ✅ 根據語法匹配types,import/require - package.json
"typesVersions"✅ - 包相對路徑 ✅ 當不存在
exports時 - 完整相對路徑 ✅
- 無副檔名相對路徑 ✅
- 目錄模組 ✅
node10 (原名為 node)
--moduleResolution node 已在 TypeScript 5.0 中重新命名為 node10(為了向後相容,保留了 node 作為別名)。它反映了 Node.js v12 之前存在的 CommonJS 模組解析演算法。不應再使用它。
支援的特性
paths✅baseUrl✅node_modules包查詢 ✅- package.json
"exports"❌ - package.json
"imports"和自引用匯入 ❌ - package.json
"typesVersions"✅ - 包相對路徑 ✅
- 完整相對路徑 ✅
- 無副檔名相對路徑 ✅
- 目錄模組 ✅
classic
不要使用 classic。