Skip to content
赞助商
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待

模块 - 参考 ​

模块语法 ​

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

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

导入和导出 TypeScript 特有的声明 ​

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

ts
// 标准 JavaScript 语法...
export function f() {}
// ...扩展到类型声明
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; // 类型 'typeof import("./module.js")' 上不存在属性 'SomeType'
let x: mod.SomeType; // 可以

仅类型导入和导出 ​

当将导入和导出输出到 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' 不能用作值,因为它是通过 'import type' 导入的
let otherFunction: typeof f = () => {}; // 可以

仅类型导入声明不能同时声明默认导入和命名绑定,因为 type 是应用于默认导入还是整个导入声明似乎是模糊的。相反,将导入声明拆分为两个,或者使用 default 作为命名绑定:

ts
import type fs, { BigIntOptions } from "fs";
//          ^^^^^^^^^^^^^^^^^^^^^
// 错误:仅类型导入可以指定默认导入或命名绑定,但不能同时指定两者。

import type { default as fs, BigIntOptions } from "fs"; // 可以

import() 类型 ​

TypeScript 提供了一种类似于 JavaScript 动态 import 的类型语法,用于在无需编写导入声明的情况下引用模块的类型:

ts
// 访问导出的类型:
type WriteFileOptions = import("fs").WriteFileOptions;
// 访问导出值的类型:
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.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; // 错误:'Options' 仅指类型,但在此处被用作值。
export = Options; // 可以

// @Filename: b.ts
const Options = require("./a");
const options: Options = { /* ... */ }; // 错误:'Options' 引用了一个值,但在此处被用作类型。

// @Filename: c.ts
import Options = require("./a");
const options: Options = { /* ... */ }; // 可以

环境模块 ​

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
// 👇 确保环境模块已加载 -
//    如果 path.d.ts 已经通过某种方式被项目 tsconfig.json 包含,则可能不需要。
/// <reference path="path.d.ts" />

import { normalize, join } from "path";

环境模块声明很容易与模块扩展混淆,因为它们使用相同的语法。当文件是模块时,即它具有顶级 import 或 export 语句(或受 --moduleDetection force 或 auto 影响),这种模块声明语法就变成了模块扩展:

ts
// 不再是环境模块声明了!
export {};
declare module "path" {
  export function normalize(p: string): string;
  export function join(...paths: any[]): string;
  export var sep: string;
}

环境模块可以在模块声明体内使用导入来引用其他模块,而不会将包含文件转换为模块(这会使环境模块声明成为模块扩展):

ts
declare module "m" {
  // 将此移到 "m" 外部将完全改变文件的含义!
  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 的最新稳定版本而变化。下表总结了三种模式当前的区别:

targetmoduleResolution导入断言导入属性JSON 导入require(esm)
node16es2022node16❌❌无限制❌
node18es2022node16✅✅需要 type "json"❌
nodenextesnextnodenext❌✅需要 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 模块的 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"; // 转换
const dynamic = import("mod"); // 不转换
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")); // 转换
const dynamic = import("mod"); // 不转换

隐含和强制选项 ​

  • --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。换句话说,每个单独的导入或导出语句的格式被保留,而不是被强制为整个编译(甚至整个文件)的单一格式。

虽然在同一文件中混合导入和 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。

在 --module preserve 中,--esModuleInterop 选项默认仅为其类型检查行为启用。由于在 --module preserve 中导入永远不会转换为 require 调用,--esModuleInterop 不会影响输出的 JavaScript。

es2015、es2020、es2022、esnext ​

总结 ​

  • 对于打包器、Bun 和 tsx,将 esnext 与 --moduleResolution bundler 一起使用。
  • 不要用于 Node.js。使用 node16、node18 或 nodenext,并在 package.json 中使用 "type": "module" 为 Node.js 输出 ES 模块。
  • 在非声明文件中不允许使用 import mod = require("mod")。
  • es2020 增加了对 import.meta 属性的支持。
  • es2022 增加了对顶层 await 的支持。
  • esnext 是一个动态目标,可能包括对 ECMAScript 模块的第 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 ​

总结 ​

  • 你可能不应该使用这个。使用 node16、node18 或 nodenext 为 Node.js 输出 CommonJS 模块。
  • 输出文件是 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 ​

总结 ​

  • 设计用于 AMD 加载器,如 RequireJS。
  • 你可能不应该使用这个。请改用打包器。
  • 输出文件是 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 查找 #1TypeScript 查找 #2TypeScript 查找 #3TypeScript 查找 #4TypeScript 查找 #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";
// 运行时查找:"./mod.js"
// TypeScript 查找 #1:"./mod.ts"
// TypeScript 查找 #2:"./mod.d.ts"
// TypeScript 查找 #3:"./mod.js"

有关为什么 TypeScript 的模块解析以这种方式工作的解释,请参阅TypeScript 模仿宿主的模块解析,但带有类型。

相对文件路径解析 ​

所有 TypeScript 的 moduleResolution 算法都支持通过包含文件扩展名的相对路径引用模块(将根据上述规则进行替换):

ts
// @Filename: a.ts
export {};

// @Filename: b.ts
import {} from "./a.js"; // ✅ 在所有 `moduleResolution` 中工作

无扩展名相对路径 ​

在某些情况下,运行时或打包器允许从相对路径中省略 .js 文件扩展名。TypeScript 在 moduleResolution 设置和上下文指示运行时或打包器支持此行为时支持此行为:

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.js 文件。TypeScript 在 moduleResolution 设置和上下文指示运行时或打包器支持此行为时支持此行为:

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
// 由于 `paths` 条目,由 ./node_modules/@types/lodash/index.d.ts 提供类型
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 是可以的,但已发布的库不应该设置 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 开始,不再需要 baseUrl 来使用 paths,并且不应仅用于设置解析 paths 值的目录。

baseUrl 编译器选项可以与任何 moduleResolution 模式结合使用,并指定一个目录,裸说明符(不以 ./、../ 或 / 开头的模块说明符)将从该目录解析。在支持 node_modules 包查找的 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_modules 包查找触发的包目录解析时,遵循 Node.js 的 package.json "exports" 规范。

TypeScript 通过 "exports" 将模块说明符解析为文件路径的实现完全遵循 Node.js。但是,一旦解析出文件路径,TypeScript 仍会尝试多种文件扩展名,以便优先找到类型。

当通过条件 "exports" 解析时,如果存在,TypeScript 总是匹配 "types" 和 "default" 条件。此外,TypeScript 将匹配形式为 "types@{selector}"(其中 {selector} 是与 "typesVersions" 兼容的版本选择器)的版本化类型条件,根据 "typesVersions" 中实现的相同版本匹配规则。其他不可配置的条件取决于 moduleResolution 模式,并在以下部分中指定。可以使用 customConditions 编译器选项配置匹配其他条件。

注意,"exports" 的存在会阻止任何未在 "exports" 中显式列出或由模式匹配的子路径被解析。

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

场景:使用 TypeScript 5.2,"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"
    }
  }
}

包目录内的解析过程:

  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" 条件 ​

场景:使用 TypeScript 5.2,"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"
      }
    }
  }
}

包目录内的解析过程:

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

包目录内的解析过程:

  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。

示例:子路径模式 ​

场景:使用 TypeScript 5.2,"pkg/wildcard.js" 被请求,条件为 ["types", "node", "import"](由 moduleResolution 设置和触发模块解析请求的上下文决定),包目录具有以下 package.json:

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 编译器版本以及对于 node_modules 包,根据正在解析的子路径,重定向 TypeScript 的解析过程。这允许包作者在一组类型定义中包含新的 TypeScript 语法,同时通过工具(如 downlevel-dts)为较旧的 TypeScript 版本提供另一组以实现向后兼容。所有 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/*"]
    }
  }
}

解析过程:

  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" 使其不必要,因为 npm 仅在 package.json 包含 "types" 字段时才在包注册表列表中显示 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,需要 `.js` 扩展名
import "pkg/dist/foo.js";             // ✅
import foo = require("pkg/dist/foo"); // ✅ require,不需要扩展名

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 源文件。

示例:带有条件的本地项目 ​

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

json5
// tsconfig.json
{
  "compilerOptions": {
    "moduleResolution": "node16",
    "resolvePackageJsonImports": true,
    "rootDir": "./src",
    "outDir": "./dist"
  }
}
json5
// 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" 中导入 "#internal/utils",条件为 ["types", "node", "import"](由 moduleResolution 设置和触发模块解析请求的上下文决定):

json5
// /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` 算法(按原样输出)
import("./mod.js");                   // 由于语法而使用 `import` 算法(按原样输出)
type Mod = typeof import("./mod.js"); // 由于文件格式而使用 `import` 算法
import mod = require("./mod");        // 由于语法而使用 `require` 算法(输出为 `require`)

// @Filename: commonjs.cts
import x from "./mod";                // 由于文件格式而使用 `require` 算法(输出为 `require`)
import("./mod.js");                   // 由于语法而使用 `import` 算法(按原样输出)
type Mod = typeof import("./mod");    // 由于文件格式而使用 `require` 算法
import mod = require("./mod");        // 由于语法而使用 `require` 算法(输出为 `require`)

隐含和强制选项 ​

支持的特性 ​

特性按优先级顺序列出。

importrequire
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 设置首先确定导入将在 .js 文件中作为 import 还是 require 调用输出,然后将该信息传递给 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 决定的假设输出,来通知模块解析和类型检查。这使得 --module preserve 成为运行时或打包器处理原始 .ts 文件时的最佳选择,因为它意味着没有转换。在 --module preserve --moduleResolution bundler 下,你可以在同一文件中编写导入和 require,它们将分别使用 import 和 require 条件解析:

ts
// index.ts
import pkg1 from "pkg";       // 使用 "import" 条件解析
import pkg2 = require("pkg"); // 使用 "require" 条件解析

隐含和强制选项 ​

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

支持的特性 ​

node10(以前称为 node) ​

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

支持的特性 ​

classic ​

不要使用 classic。