模块 - 参考

模块语法

TypeScript 编译器能识别 TypeScript 和 JavaScript 文件中的标准 ECMAScript 模块语法,以及 JavaScript 文件中多种形式的 CommonJS 语法。

此外还有一些 TypeScript 特有的语法扩展,可用于 TypeScript 文件和/或 JSDoc 注释中。

导入和导出 TypeScript 特有的声明

类型别名、接口、枚举和命名空间可以像任何标准 JavaScript 声明一样,通过 export 修饰符从模块中导出:

ts
// Standard JavaScript syntax...
export function f() {}
// ...extended to type declarations
export type SomeType = /* ... */;
export interface SomeInterface { /* ... */ }

它们也可以出现在具名导出中,甚至可以与对标准 JavaScript 声明的引用并列:

ts
export { f, SomeType, SomeInterface };

导出的类型(以及其他 TypeScript 特有的声明)可以用标准 ECMAScript 导入语句导入:

ts
import { f, SomeType, SomeInterface } from "./module.js";

在使用命名空间导入或导出时,如果导出的类型在类型位置被引用,它们可以从命名空间上访问:

ts
import * 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.ts
import { 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.js
import { f } from "./module.js";
class C {
constructor(p) {
f();
}
}

甚至值也可以用 import type 导入,但由于它们不会存在于输出的 JavaScript 中,因此只能在非输出位置使用:

ts
import 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 用作具名绑定:

ts
import 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.ts
import 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.ts
interface Options { /* ... */ }
module.exports = Options; // Error: 'Options' only refers to a type, but is being used as a value here.
export = Options; // Ok
// @Filename: b.ts
const Options = require("./a");
const options: Options = { /* ... */ }; // Error: 'Options' refers to a value, but is being used as a type here.
// @Filename: c.ts
import Options = require("./a");
const options: Options = { /* ... */ }; // Ok

环境模块

TypeScript 在脚本(非模块)文件中支持一种语法,用于声明一个存在于运行时但没有任何对应文件的模块。这些环境模块通常表示由运行时提供的模块,例如 Node.js 中的 "fs" 或 "path":

ts
declare 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";

环境模块声明很容易与模块扩充混淆,因为二者使用完全相同的语法。当文件本身是模块时——也就是说,它含有顶层的 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;
}

环境模块可以在模块声明体内部使用导入来引用其他模块,同时不会让所在文件变成模块(否则该环境模块声明就会变成模块扩充):

ts
declare module "m" {
// Moving this outside "m" would totally change the meaning of the file!
import { SomeType } from "other";
export function f(): SomeType;
}

模式环境模块的名称中包含一个 * 通配符字符,可匹配导入路径中零个或多个字符。这对于声明由自定义加载器提供的模块很有用:

ts
declare 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 或更高版本中运行的应用和库来说,无论是否使用 ES 模块,它们都是唯一正确的 module 选项。

固定版本的 node16 和 node18 模式表示在各自 Node.js 版本中已稳定的模块系统行为,而 nodenext 模式会随 Node.js 最新的稳定版本而变化。下表总结了这三种模式当前的差异:

target moduleResolution 导入断言 导入属性 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 模块。
  • .ts/.tsx/.js/.jsx/.d.ts 文件在最近的祖先 package.json 文件包含 "type": "module" 时是 ES 模块,否则是 CommonJS 模块。

输入 .ts/.tsx/.mts/.cts 文件检测到的模块格式决定了输出 JavaScript 文件的模块格式。因此,例如,一个完全由 .ts 文件组成的项目在 --module nodenext 下默认会输出全部为 CommonJS 模块,而在项目的 package.json 中添加 "type": "module" 就能让它输出全部为 ES 模块。

互操作规则

  • 当 ES 模块引用 CommonJS 模块时:
    • CommonJS 模块的 module.exports 可以作为默认导入供 ES 模块使用。
    • CommonJS 模块 module.exports 的属性(default 除外)可能会、也可能不会作为具名导入供 ES 模块使用。Node.js 会尝试通过静态分析让它们可用。TypeScript 无法从声明文件得知该静态分析是否会成功,因此乐观地假定它会成功。这限制了 TypeScript 捕获那些可能在运行时崩溃的具名导入的能力。更多详情请参阅 #54018。
  • 当 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 调用的结果是该模块的模块命名空间对象,即与对同一模块执行 await import() 的结果相同(但无需 await 任何东西)。
    • 动态 import() 调用始终可用于导入 ES 模块。它返回该模块的模块命名空间对象的 Promise(与从另一个 ES 模块执行 import * as ns from "./module.js" 得到的结果相同)。

输出

每个文件的输出格式由该文件的检测到的模块格式决定。ESM 输出类似于 --module esnext,但对 import x = require("...") 有特殊的转换,而后者在 --module esnext 中是不允许的:

ts
// @Filename: main.ts
import x = require("mod");
js
// @Filename: main.js
import { createRequire as _createRequire } from "module";
const __require = _createRequire(import.meta.url);
const x = __require("mod");

CommonJS 输出类似于 --module commonjs,但动态 import() 调用不会被转换。此处的输出是在启用 esModuleInterop 时展示的:

ts
// @Filename: main.ts
import fs from "fs"; // transformed
const 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")); // transformed
const 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。

小结

  • 对于所有打算在 Node.js v12 或更高版本中运行的应用和库,无论是否使用 ES 模块,node16、node18 和 nodenext 都是唯一正确的 module 选项。
  • 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 形式输出。换句话说,每条导入或导出语句的格式都会被保留,而不会被强制统一为整个编译(甚至整个文件)的单一格式。

虽然很少需要在同一文件中混用导入和 require 调用,但这种 module 模式最能反映大多数现代打包器以及 Bun 运行时的能力。

既然使用打包器或 Bun 时你可能还会设置 noEmit,为什么还要关心 TypeScript 的 module 输出呢?TypeScript 的类型检查和模块解析行为会受到它将会输出的模块格式影响。设置 module 能让 TypeScript 了解你的打包器或运行时将如何处理导入和导出,从而确保你在导入值上看到的类型准确反映运行时或打包后的实际情况。更多讨论请参阅 --moduleResolution bundler。

示例

ts
// @Filename: main.ts
import x, { y, z } from "mod";
import mod = require("mod");
const dynamic = import("mod");
export const e1 = 0;
export default "default export";
js
// @Filename: main.js
import 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.ts
import 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
import 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

小结

  • 你大概不应该使用它。要在 Node.js 中输出 CommonJS 模块,请使用 node16、node18 或 nodenext。
  • 输出文件是 CommonJS 模块,但依赖可以是任意格式。
  • 动态 import() 会被转换为 require() 调用构成的 Promise。
  • esModuleInterop 会影响默认导入和命名空间导入的输出代码。

示例

输出是在 esModuleInterop: false 下展示的。

ts
// @Filename: main.ts
import 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.ts
import 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

小结

示例

ts
// @Filename: main.ts
import 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
System.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.ts
import 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
define(["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.ts
import 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

请注意,这一行为与导入中实际写出的模块说明符无关。这意味着即使模块说明符显式使用了 .js 文件扩展名,TypeScript 也能解析到 .ts 或 .d.ts 文件:

ts
import 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.ts
export {};
// @Filename: b.ts
import {} from "./a.js"; // ✅ Works in every `moduleResolution`

无扩展名的相对路径

在某些情况下,运行时或打包器允许在相对路径中省略 .js 文件扩展名。当 moduleResolution 设置和上下文表明运行时或打包器支持该行为时,TypeScript 也会支持它:

ts
// @Filename: a.ts
export {};
// @Filename: b.ts
import {} from "./a";

如果 TypeScript 判定在给定模块说明符 "./a" 时运行时会查找 ./a.js,那么 ./a.js 将经过 扩展名替换,在本例中解析到文件 a.ts。

Node.js 中的 import 路径不支持无扩展名的相对路径,package.json 文件中指定的文件路径也并不总是支持。TypeScript 目前从不支持省略 .mjs/.mts 或 .cjs/.cts 文件扩展名,尽管有些运行时和打包器支持这样做。

目录模块(index 文件解析)

在某些情况下,可以把目录而非文件引用为模块。最简单也最常见的情形是运行时或打包器在目录中查找 index.js 文件。当 moduleResolution 设置和上下文表明运行时或打包器支持该行为时,TypeScript 也会支持它:

ts
// @Filename: dir/index.ts
export {};
// @Filename: b.ts
import {} 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 编译器选项覆盖编译器对裸说明符的模块解析。尽管该特性最初是为 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` entry
import { 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 是可以的,但已发布的库不要这样做,这一点非常重要,因为如果库的使用者没有为自己的 TypeScript 和打包器设置相同的别名,输出的 JavaScript 就无法在这些使用者那里工作。库和应用都可以考虑把 package.json "imports" 作为便捷 paths 别名的标准替代方案。

paths 不应指向 monorepo 包或 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,而且从该包导入在运行时可能会失败。

同样的注意事项也适用于 monorepo 中相互引用的包。与其使用 paths 让 TypeScript 人为地把 "@my-scope/lib" 解析为同级包,不如通过 npm、yarn 或 pnpm 的工作区把你的包符号链接到 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/" 和 "" 都长。

回退

一个路径映射可以提供多个文件路径。如果某个路径解析失败,将尝试数组中的下一个路径,直到解析成功或到达数组末尾。

json
{
"compilerOptions": {
"paths": {
"*": ["./vendor/*", "./types/*"]
}
}
}

baseUrl

baseUrl 是为配合 AMD 模块加载器而设计的。如果你没有使用 AMD 模块加载器,可能就不应该使用 baseUrl。从 TypeScript 4.1 开始,使用 paths 不再需要 baseUrl,也不应仅仅为了设置 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" 查找等优先级更高的裸说明符规则用尽之后开始):

  1. 对于进行导入的文件的每个祖先目录,如果其中存在 node_modules 目录:
    1. 如果 node_modules 中存在与包同名的目录:
      1. 尝试从该包目录解析类型。
      2. 如果找到结果,则返回该结果并停止搜索。
    2. 如果 node_modules/@types 中存在与包同名的目录:
      1. 尝试从 @types 包目录解析类型。
      2. 如果找到结果,则返回该结果并停止搜索。
  2. 在遍历所有 node_modules 目录时重复之前的搜索,但这次允许结果中包含 JavaScript 文件,并且不在 @types 目录中搜索。

所有 moduleResolution 模式(classic 除外)都遵循这一模式,但它们在定位到包目录后如何解析的细节各不相同,将在后续章节中说明。

package.json 的 "exports"

当 moduleResolution 设为 node16、nodenext 或 bundler,且 resolvePackageJsonExports 未禁用时,TypeScript 在由包目录发起的解析中会遵循 Node.js 的 package.json "exports" 规范,而该解析是由裸说明符 node_modules 包查找触发的。

TypeScript 通过 "exports" 把模块说明符解析为文件路径的实现与 Node.js 完全一致。不过,一旦文件路径解析完成,TypeScript 仍会尝试多个文件扩展名,以便优先找到类型。

当通过 条件 "exports" 进行解析时,TypeScript 始终会匹配 "types" 和 "default" 条件(如果存在)。此外,TypeScript 还会按照 "typesVersions" 中实现的相同版本匹配规则,匹配形式为 "types@{selector}" 的带版本 types 条件(其中 {selector} 是兼容 "typesVersions" 的版本选择器)。其他不可配置的条件取决于 moduleResolution 模式,并在后续各节中说明。还可以通过 customConditions 编译器选项配置要匹配的附加条件。

请注意,只要存在 "exports",任何未在 "exports" 中显式列出、也未被其中某个模式匹配的子路径都无法被解析。

示例:子路径、条件与扩展名替换

场景:在包含以下 package.json 的包目录中,以条件 ["types", "node", "require"](由 moduleResolution 设置以及触发该模块解析请求的上下文决定)请求 "pkg/subpath":

json
{
"name": "pkg",
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.cjs"
},
"./subpath": {
"import": "./subpath/index.mjs",
"require": "./subpath/index.cjs"
}
}
}

包目录内的解析过程:

  1. 是否存在 "exports"?是。
  2. "exports" 是否有 "./subpath" 条目?是。
  3. exports["./subpath"] 处的值是一个对象——它必定是在指定条件。
  4. 第一个条件 "import" 是否匹配此请求?否。
  5. 第二个条件 "require" 是否匹配此请求?是。
  6. 路径 "./subpath/index.cjs" 是否具有可识别的 TypeScript 文件扩展名?否,因此使用扩展名替换。
  7. 通过扩展名替换,依次尝试以下路径,返回其中第一个存在的路径,否则返回 undefined:
    1. ./subpath/index.cts
    2. ./subpath/index.d.cts
    3. ./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 的包目录中,以条件 ["types", "node", "import"](由 moduleResolution 设置以及触发该模块解析请求的上下文决定)请求 "pkg/subpath":

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"
}
}
}
}

包目录内的解析过程:

  1. 是否存在 "exports"?是。
  2. "exports" 是否有 "./subpath" 条目?是。
  3. exports["./subpath"] 处的值是一个对象——它必定是在指定条件。
  4. 第一个条件 "import" 是否匹配此请求?是。
  5. exports["./subpath"].import 处的值是一个对象——它必定是在指定条件。
  6. 第一个条件 "types" 是否匹配此请求?是。
  7. 路径 "./types/subpath/index.d.mts" 是否具有可识别的 TypeScript 文件扩展名?是,因此不要使用扩展名替换。
  8. 如果文件存在,则返回路径 "./types/subpath/index.d.mts",否则返回 undefined。
示例:带版本的 "types" 条件

场景:使用 TypeScript 4.7.5,在包含以下 package.json 的包目录中,以条件 ["types", "node", "import"](由 moduleResolution 设置以及触发该模块解析请求的上下文决定)请求 "pkg/subpath":

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"
}
}
}

包目录内的解析过程:

  1. 是否存在 "exports"?是。
  2. "exports" 是否有 "./subpath" 条目?是。
  3. exports["./subpath"] 处的值是一个对象——它必定是在指定条件。
  4. 第一个条件 "types@>=5.2" 是否匹配此请求?否,4.7.5 并不大于或等于 5.2。
  5. 第二个条件 "types@>=4.6" 是否匹配此请求?是,4.7.5 大于或等于 4.6。
  6. 路径 "./ts4.6/subpath/index.d.ts" 是否具有可识别的 TypeScript 文件扩展名?是,因此不要使用扩展名替换。
  7. 如果文件存在,则返回路径 "./ts4.6/subpath/index.d.ts",否则返回 undefined。
示例:子路径模式

场景:在包含以下 package.json 的包目录中,以条件 ["types", "node", "import"](由 moduleResolution 设置以及触发该模块解析请求的上下文决定)请求 "pkg/wildcard.js":

json
{
"name": "pkg",
"type": "module",
"exports": {
"./*.js": {
"types": "./types/*.d.ts",
"default": "./dist/*.js"
}
}
}

包目录内的解析过程:

  1. 是否存在 "exports"?是。
  2. "exports" 是否有 "./wildcard.js" 条目?否。
  3. 是否有任何包含 * 的键匹配 "./wildcard.js"?是,"./*.js" 匹配成功,并把替换内容设为 wildcard。
  4. exports["./*.js"] 处的值是一个对象——它必定是在指定条件。
  5. 第一个条件 "types" 是否匹配此请求?是。
  6. 在 ./types/*.d.ts 中,用替换内容 wildcard 替换 *。./types/wildcard.d.ts
  7. 路径 "./types/wildcard.d.ts" 是否具有可识别的 TypeScript 文件扩展名?是,因此不要使用扩展名替换。
  8. 如果文件存在,则返回路径 "./types/wildcard.d.ts",否则返回 undefined。
示例:"exports" 阻止其他子路径

场景:在包含以下 package.json 的包目录中请求 "pkg/dist/index.js":

json
{
"name": "pkg",
"main": "./dist/index.js",
"exports": "./dist/index.js"
}

包目录内的解析过程:

  1. 是否存在 "exports"?是。
  2. exports 处的值是一个字符串——它必定是包根(".")的文件路径。
  3. 请求 "pkg/dist/index.js" 是否针对包根?否,它带有子路径 dist/index.js。
  4. 解析失败;返回 undefined。

如果没有 "exports",该请求本可以成功,但 "exports" 的存在使得任何无法通过 "exports" 匹配的子路径都无法被解析。

package.json 的 "typesVersions"

node_modules 包或目录模块可以在其 package.json 中指定 "typesVersions" 字段,以便根据 TypeScript 编译器的版本重定向 TypeScript 的解析过程;对于 node_modules 包,还会根据正在解析的子路径进行重定向。这使包作者可以在一组类型定义中包含新的 TypeScript 语法,同时提供另一组用于向后兼容旧版 TypeScript(通过 downlevel-dts 之类的工具)。"typesVersions" 在所有 moduleResolution 模式下都受支持;不过,在读取 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/*"]
}
}
}

解析过程:

  1. (取决于编译器选项)是否存在 "exports"?不存在。
  2. 是否存在 "typesVersions"?存在。
  3. TypeScript 版本是否 >=3.1?是。记住映射 "*": ["ts3.1/*"]。
  4. 我们是否在解析包名之后的子路径?不是,只是根路径 "pkg"。
  5. 是否存在 "types"?存在。
  6. "typesVersions" 中的任何键是否匹配 ./index.d.ts?是,"*" 匹配,并将 index.d.ts 设为替换内容。
  7. 在 ts3.1/* 中,将 * 替换为替换内容 ./index.d.ts:ts3.1/index.d.ts。
  8. 路径 ./ts3.1/index.d.ts 是否具有可识别的 TypeScript 文件扩展名?是,因此不要使用扩展名替换。
  9. 如果文件存在则返回路径 ./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"] }
}
}

解析过程:

  1. (取决于编译器选项)是否存在 "exports"?不存在。
  2. 是否存在 "typesVersions"?存在。
  3. TypeScript 版本是否 <4.0?是。记住映射 "index.d.ts": ["index.v3.d.ts"]。
  4. 我们是否在解析包名之后的子路径?不是,只是根路径 "pkg"。
  5. 是否存在 "types"?存在。
  6. "typesVersions" 中的任何键是否匹配 ./index.d.ts?是,"index.d.ts" 匹配。
  7. 路径 ./index.v3.d.ts 是否具有可识别的 TypeScript 文件扩展名?是,因此不要使用扩展名替换。
  8. 如果文件存在则返回路径 ./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.mts
import "pkg/dist/foo"; // ❌ import, needs `.js` extension
import "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" 字段的值)开头的导入路径。这两项功能都允许包内文件导入同一包中的其他文件,从而替代相对导入路径。

对于 "imports" 和自引用,TypeScript 完全遵循 Node.js 的解析算法,直到解析出文件路径为止。此时,TypeScript 的解析算法会分叉,具体取决于正在解析的 "imports" 或 "exports" 所在的 package.json 属于 node_modules 依赖,还是属于正在编译的本地项目(即它的目录中包含发起导入的文件所在项目的 tsconfig.json 文件):

  • 如果 package.json 位于 node_modules 中,且文件路径尚未带有可识别的 TypeScript 文件扩展名,TypeScript 就会对该路径应用扩展名替换,并检查得到的各个文件路径是否存在。
  • 如果 package.json 属于本地项目,则会额外执行一次重映射,以找到输入的 TypeScript 实现文件,该文件最终会产出从 "imports" 解析得到的输出 JavaScript 或声明文件路径。没有这一步,任何解析 "imports" 路径的编译都会引用上一次编译的输出文件,而不是本应包含在当前编译中的其他输入文件。此重映射使用 tsconfig.json 中的 outDir/declarationDir 和 rootDir,因此使用 "imports" 通常需要显式设置 rootDir。

这一差异让包作者可以编写仅引用将要发布到 npm 的编译产物的 "imports" 和 "exports" 字段,同时仍允许本地开发使用原始的 TypeScript 源文件。

示例:带条件的本地项目

场景:在包含 tsconfig.json 和 package.json 的项目目录中,"/src/main.mts" 以条件 ["types", "node", "import"](由 moduleResolution 设置以及触发该模块解析请求的上下文决定)导入 "#utils":

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"
}
}
}

解析过程:

  1. 导入路径以 # 开头,尝试通过 "imports" 进行解析。
  2. 最近的祖先 package.json 中是否存在 "imports"?是。
  3. "imports" 对象中是否存在 "#utils"?是。
  4. imports["#utils"] 处的值是一个对象——它必定是在指定条件。
  5. 第一个条件 "import" 是否匹配此请求?是。
  6. 是否应尝试将输出路径映射为输入路径?是,因为:
    • package.json 是否位于 node_modules 中?不是,它位于本地项目中。
    • tsconfig.json 是否位于 package.json 所在目录中?是。
  7. 在 ./dist/utils.d.mts 中,将 outDir 前缀替换为 rootDir。./src/utils.d.mts
  8. 将输出扩展名 .d.mts 替换为对应的输入扩展名 .mts。./src/utils.mts
  9. 如果该文件存在,则返回路径 "./src/utils.mts"。
  10. 否则,如果该文件存在,则返回路径 "./dist/utils.d.mts"。
示例:带子路径模式的 node_modules 依赖

场景:package.json 如下,"/node_modules/pkg/main.mts" 以条件 ["types", "node", "import"](由 moduleResolution 设置以及触发该模块解析请求的上下文决定)导入 "#internal/utils":

json
// /node_modules/pkg/package.json
{
"name": "pkg",
"imports": {
"#internal/*": {
"import": "./dist/internal/*.mjs",
"require": "./dist/internal/*.cjs"
}
}
}

解析过程:

  1. 导入路径以 # 开头,尝试通过 "imports" 进行解析。
  2. 最近的祖先 package.json 中是否存在 "imports"?是。
  3. "imports" 对象中是否存在 "#internal/utils"?不存在,检查模式匹配。
  4. 是否有带 * 的键匹配 "#internal/utils"?是,"#internal/*" 匹配,并将 utils 设为替换内容。
  5. imports["#internal/*"] 处的值是一个对象——它必定是在指定条件。
  6. 第一个条件 "import" 是否匹配此请求?是。
  7. 是否应尝试将输出路径映射为输入路径?否,因为 package.json 位于 node_modules 中。
  8. 在 ./dist/internal/*.mjs 中,将 * 替换为替换内容 utils。./dist/internal/utils.mjs
  9. 路径 ./dist/internal/utils.mjs 是否带有可识别的 TypeScript 文件扩展名?否,尝试扩展名替换。
  10. 通过扩展名替换,依次尝试以下路径,返回第一个存在的路径,否则返回 undefined:
    1. ./dist/internal/utils.mts
    2. ./dist/internal/utils.d.mts
    3. ./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.mts
import 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 format
import mod = require("./mod"); // `require` algorithm due to syntax (emitted as `require`)
// @Filename: commonjs.cts
import 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 format
import mod = require("./mod"); // `require` algorithm due to syntax (emitted as `require`)

隐含与强制选项

支持的特性

特性按优先级顺序列出。

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 文件中的一条导入语句:

ts
// index.ts
import { 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 还是 require 调用的规则会有所不同,因为 --moduleResolution bundler 必须配合 --module esnext 或 --module preserve 使用。在这两种模式下,ESM 的 import 声明总是作为 ESM import 声明输出,因此 TypeScript 的模块解析器会收到该信息,并在解析 "pkg" 的 "exports" 时使用 "import" 条件。

这种解释可能有些不太直观,因为 --moduleResolution bundler 通常与 --noEmit 一起使用——打包器通常会处理原始的 .ts 文件,并对未转换的 import 或 require 执行模块解析。不过,为了一致性,TypeScript 仍会使用由 module 决定的假设输出来指导模块解析和类型检查。这使得只要运行时或打包器处理的是原始 .ts 文件,--module preserve 就是最佳选择,因为它意味着不进行任何转换。在 --module preserve --moduleResolution bundler 下,你可以在同一个文件中编写导入和 require,它们将分别使用 import 和 require 条件进行解析:

ts
// index.ts
import pkg1 from "pkg"; // Resolved with "import" condition
import pkg2 = require("pkg"); // Resolved with "require" condition

隐含与强制选项

  • --moduleResolution bundler 必须与 --module esnext 或 --module preserve 搭配使用。
  • --moduleResolution bundler 隐含 --allowSyntheticDefaultImports。

支持的特性

node10(以前称为 node)

--moduleResolution node 在 TypeScript 5.0 中更名为 node10(为向后兼容保留了 node 作为别名)。它反映的是 Node.js v12 之前版本中存在的 CommonJS 模块解析算法。不应再使用它。

支持的特性

classic

不要使用 classic。

TypeScript 文档是一个开源项目。欢迎通过提交 Pull Request 帮助我们改进这些页面 ❤

最后更新:2026 年 10 月 23 日