模块 - 参考

模块语法

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

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

导入和导出特定于 TypeScript 的声明

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

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 文件可以使用 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("...");

这种语法之所以被使用,是因为变量声明和属性赋值不能引用 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";

环境模块声明很容易与 模块增强 混淆,因为它们使用相同的语法。当文件是模块时,此模块声明语法变为模块增强,这意味着它具有顶级 importexport 语句(或受 --moduleDetection forceauto 影响)

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 文件的输出模块格式。但是,最近的 node16nodenext 值描述了 Node.js 模块系统的广泛特征,包括支持哪些模块格式、如何确定每个文件的模块格式以及不同模块格式如何交互。

node16, nodenext

Node.js 支持 CommonJS 和 ECMAScript 模块,并有特定规则来规定每个文件可以采用哪种格式以及两种格式如何交互。node16nodenext 描述了 Node.js 双格式模块系统的全部行为,并以 CommonJS 或 ESM 格式发出文件。这与其他所有 module 选项不同,这些选项与运行时无关,并强制所有输出文件采用单一格式,由用户确保输出对他们的运行时有效。

一个常见的误解是,node16nodenext 仅发出 ES 模块。实际上,node16nodenext 描述的是支持 ES 模块的 Node.js 版本,而不仅仅是使用 ES 模块的项目。根据每个文件的检测到的模块格式,ESM 和 CommonJS 发射都受支持。由于 node16nodenext 是唯一反映 Node.js 双模块系统复杂性的 module 选项,因此它们是所有旨在在 Node.js v12 或更高版本中运行的应用程序和库的唯一正确的 module 选项,无论它们是否使用 ES 模块。

node16nodenext 目前是相同的,区别在于它们暗示不同的 target 选项值。如果 Node.js 在未来对模块系统进行重大更改,node16 将被冻结,而 nodenext 将被更新以反映新的行为。

模块格式检测

  • .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 模块时
    • require 无法引用 ES 模块。对于 TypeScript,这包括 检测 为 CommonJS 模块的文件中的 import 语句,因为这些 import 语句将在发出的 JavaScript 中转换为 require 调用。
    • 可以使用动态 import() 调用导入 ES 模块。它返回一个 Promise,该 Promise 包含模块的模块命名空间对象(您从另一个 ES 模块的 import * as ns from "./module.js" 中获得的内容)。

发出

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

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

CommonJS 发出类似于 --module commonjs,但动态 import() 调用不会被转换。这里显示的发出是在启用 esModuleInterop 的情况下。

ts
import fs from "fs"; // transformed
const dynamic = import("mod"); // not transformed
js
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = __importDefault(require("fs")); // transformed
const dynamic = import("mod"); // not transformed

隐含和强制选项

  • --module nodenextnode16 意味着并强制使用同名的 moduleResolution
  • --module nodenext 意味着 --target esnext
  • --module node16 意味着 --target es2022
  • --module nodenextnode16 意味着 --esModuleInterop

总结

  • node16nodenext 是所有旨在在 Node.js v12 或更高版本中运行的应用程序和库的唯一正确的 module 选项,无论它们是否使用 ES 模块。
  • node16nodenext 会根据每个文件的 检测到的模块格式 以 CommonJS 或 ESM 格式发出文件。
  • Node.js 中 ESM 和 CJS 之间的互操作性规则反映在类型检查中。
  • ESM 发出将 import x = require("...") 转换为从 createRequire 导入构建的 require 调用的转换。
  • CommonJS 发出将动态 import() 调用保持不变,因此 CommonJS 模块可以异步导入 ES 模块。

es2015es2020es2022esnext

总结

  • 对于打包工具、Bun 和 tsx,请使用 esnext 以及 --moduleResolution bundler
  • 不要用于 Node.js。对于 Node.js,请使用 node16nodenext,并在 package.json 中使用 "type": "module" 来输出 ES 模块。
  • 在非声明文件中不允许使用 import mod = require("mod")
  • es2020 添加了对 import.meta 属性的支持。
  • es2022 添加了对顶层 await 的支持。
  • esnext 是一个不断变化的目标,可能包括对 ECMAScript 模块的 Stage 3 提案的支持。
  • 输出文件为 ES 模块,但依赖项可以是任何格式。

示例

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

总结

  • 您可能不应该使用它。请使用 node16nodenext 来为 Node.js 输出 CommonJS 模块。
  • 输出文件为 CommonJS 模块,但依赖项可以是任何格式。
  • 动态 import() 将转换为 require() 调用的 Promise。
  • esModuleInterop 会影响默认和命名空间导入的输出代码。

示例

输出显示了 esModuleInterop: false

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
"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
import mod = require("mod");
console.log(mod);
export = {
p1: true,
p2: false
};
js
"use strict";
const mod = require("mod");
console.log(mod);
module.exports = {
p1: true,
p2: false
};

system

概述

示例

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
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
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
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
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
(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` 文件扩展名。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/[email protected]": ["./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/[email protected]";

使用打包工具构建的应用程序通常会在其打包工具配置中定义方便的路径别名,然后使用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,因为输出的 JavaScript 代码对于库的使用者来说将无法正常工作,除非这些使用者为 TypeScript 和他们的打包工具设置相同的别名。库和应用程序都可以考虑使用package.json "imports"作为方便的paths别名的标准替代方案。

paths不应该指向单仓库项目中的包或node_modules中的包。

虽然与paths别名匹配的模块标识符是裸标识符,但一旦别名解析完成,模块解析将以解析后的路径作为相对路径进行。因此,针对node_modules包查找的解析功能(包括对 package.json "exports" 字段的支持)在匹配paths别名时不会生效。如果使用paths指向node_modules包,这可能会导致意外的行为。

ts
{
"compilerOptions": {
"paths": {
"pkg": ["./node_modules/pkg/dist/index.d.ts"],
"pkg/*": ["./node_modules/pkg/*"]
}
}
}

虽然这种配置可以模拟一些包解析的行为,但它会覆盖包的package.json文件中定义的任何maintypesexportstypesVersions,并且来自该包的导入可能会在运行时失败。

对于在单仓库中相互引用的包,也存在同样的注意事项。与其使用 `paths` 让 TypeScript 人工地将 `"@my-scope/lib"` 解析到兄弟包,不如通过 npmyarnpnpm 使用工作区将你的包符号链接到 `node_modules` 中,这样 TypeScript 和运行时或捆绑器就可以执行真正的 `node_modules` 包查找。这在单仓库包将发布到 npm 时尤其重要,因为一旦用户安装,这些包将通过 `node_modules` 包查找相互引用,使用工作区允许你在本地开发期间测试这种行为。

与 `baseUrl` 的关系

当提供 `baseUrl` 时,每个 `paths` 数组中的值将相对于 `baseUrl` 解析。否则,它们将相对于定义它们的 `tsconfig.json` 文件解析。

通配符替换

`paths` 模式可以包含单个 `*` 通配符,它匹配任何字符串。然后可以在文件路径值中使用 `*` 符号来替换匹配的字符串。

json
{
"compilerOptions": {
"paths": {
"@app/*": ["./src/*"]
}
}
}

在解析 `"@app/components/Button"` 的导入时,TypeScript 将匹配 `@app/*`,将 `*` 绑定到 `components/Button`,然后尝试相对于 `tsconfig.json` 路径解析路径 `./src/components/Button`。此查找的其余部分将遵循与任何其他 相对路径查找 相同的规则,具体取决于 `moduleResolution` 设置。

当多个模式匹配模块说明符时,将使用在任何*标记之前具有最长匹配前缀的模式。

json
{
"compilerOptions": {
"paths": {
"*": ["./src/foo/one.ts"],
"foo/*": ["./src/foo/two.ts"],
"foo/bar": ["./src/foo/three.ts"]
}
}
}

在解析"foo/bar"的导入时,所有三个paths模式都匹配,但使用最后一个,因为"foo/bar""foo/"""更长。

回退

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

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

baseUrl

baseUrl专为与AMD模块加载器一起使用而设计。如果您没有使用AMD模块加载器,则可能不应该使用baseUrl。从TypeScript 4.1开始,不再需要baseUrl来使用paths,也不应该仅用于设置解析paths值的目录。

baseUrl编译器选项可以与任何moduleResolution模式结合使用,并指定一个目录,从该目录解析裸说明符(不以./..//开头的模块说明符)。baseUrlnode_modules包查找具有更高的优先级,在支持它们的moduleResolution模式中。

在执行baseUrl查找时,解析过程与其他相对路径解析过程遵循相同的规则。例如,在支持无扩展名相对路径moduleResolution模式中,模块说明符"some-file"可能会解析为/src/some-file.ts,如果baseUrl设置为/src

相对模块说明符的解析永远不会受到baseUrl选项的影响。

node_modules包查找

Node.js 将非相对路径、绝对路径或 URL 的模块标识符视为对包的引用,它会在 node_modules 子目录中查找这些包。捆绑器方便地采用了这种行为,允许其用户使用与在 Node.js 中相同的依赖项管理系统,甚至经常使用相同的依赖项。除了 classic 之外,TypeScript 的所有 moduleResolution 选项都支持 node_modules 查找。(classic 在其他解析方法失败时支持在 node_modules/@types 中查找,但从不直接在 node_modules 中查找包。)每次 node_modules 包查找都具有以下结构(从优先级更高的裸标识符规则开始,例如 pathsbaseUrl、自命名导入和 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 设置为 node16nodenextbundler,并且 resolvePackageJsonExports 未禁用时,TypeScript 会遵循 Node.js 的 package.json "exports" 规范,从由 裸标识符 node_modules 包查找 触发的包目录解析。

TypeScript 通过 "exports" 将模块标识符解析为文件路径的实现完全遵循 Node.js。但是,一旦解析了文件路径,TypeScript 仍然会 尝试多个文件扩展名,以优先查找类型。

在通过 条件化的 "exports" 解析时,TypeScript 始终会匹配 "types""default" 条件(如果存在)。此外,TypeScript 会根据 "typesVersions" 中实现的相同版本匹配规则,匹配 "types@{selector}" 形式的版本化类型条件(其中 {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,使用条件 ["types", "node", "import"](由 moduleResolution 设置和触发模块解析请求的上下文决定)请求 "pkg/subpath" 在具有以下 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
示例:子路径模式

场景:在具有以下 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

"types" 的永久链接" class="anchor before">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"] 的模式下,即使导入无法通过 "exports" 解析,仅仅存在包的 package.json 中的 "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 设置为 node16nodenextbundler,并且 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/declarationDirrootDir,因此使用 "imports" 通常需要显式设置 rootDir

这种变体允许包作者编写仅引用将发布到 npm 的编译输出的 "imports""exports" 字段,同时仍然允许本地开发使用原始 TypeScript 源文件。

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

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

json
// tsconfig.json
{
"compilerOptions": {
"moduleResolution": "node16",
"resolvePackageJsonImports": true,
"rootDir": "./src",
"outDir": "./dist"
}
}
json
// package.json
{
"name": "pkg",
"imports": {
"#utils": {
"import": "./dist/utils.d.mts",
"require": "./dist/utils.d.cts"
}
}
}

解析过程

  1. 导入路径以 # 开头,尝试通过 "imports" 解析。
  2. "imports" 是否存在于最近的祖先 package.json 中?**是。**
  3. "#utils" 是否存在于 "imports" 对象中?**是。**
  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 依赖项

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

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

解析过程

  1. 导入路径以 # 开头,尝试通过 "imports" 解析。
  2. "imports" 是否存在于最近的祖先 package.json 中?**是。**
  3. "#internal/utils" 是否存在于 "imports" 对象中?否,检查模式匹配。
  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 及更高版本中的模块解析行为。(node16nodenext 目前相同,但如果 Node.js 对其模块系统进行了重大更改,node16 将被冻结,而 nodenext 将被更新以反映新的行为。)在 Node.js 中,ECMAScript 导入的解析算法与 CommonJS require 调用的算法有很大不同。对于每个要解析的模块标识符,首先使用导入文件的语法和模块格式来确定模块标识符将在生成的 JavaScript 中是 import 还是 require。然后将该信息传递给模块解析器,以确定使用哪种解析算法(以及是否对 package.json "exports""imports" 使用 "import""require" 条件)。

默认情况下,被确定为 CommonJS 格式 的 TypeScript 文件仍然可以使用 importexport 语法,但生成的 JavaScript 将使用 requiremodule.exports。这意味着通常会看到使用 require 算法解析的 import 语句。如果这会导致混淆,可以启用 verbatimModuleSyntax 编译器选项,该选项禁止使用将被发出为 require 调用的 import 语句。

请注意,动态import()调用始终使用import算法解析,符合 Node.js 的行为。但是,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" ✅ 匹配 typesnodeimport ✅ 匹配 typesnoderequire
package.json "imports" 和自命名导入 ✅ 匹配 typesnodeimport ✅ 匹配 typesnoderequire
package.json "typesVersions"
包相对路径 ✅ 当 exports 不存在时 ✅ 当 exports 不存在时
完整相对路径
无扩展名相对路径
目录模块

bundler

--moduleResolution bundler 尝试模拟大多数 JavaScript 打包器常用的模块解析行为。简而言之,这意味着支持传统上与 Node.js 的 CommonJS require 解析算法相关的所有行为,例如 node_modules 查找目录模块无扩展名路径,同时还支持 Node.js 的较新解析功能,例如 package.json "exports"package.json "imports"

这与 node16nodenext 在 CommonJS 模式下解析的行为非常相似,但在 bundler 中,用于解析 package.json "exports""imports" 的条件始终为 "types""import"。为了理解原因,让我们比较一下在 nodenext.ts 文件中的导入会发生什么。

ts
// index.ts
import { foo } from "pkg";

--module nodenext --moduleResolution nodenext 中,--module 设置首先 确定 导入将以 importrequire 调用形式发出到 .js 文件中,并将该信息传递给 TypeScript 的模块解析器,该解析器决定是否相应地匹配 "import""require" 条件。这确保了 TypeScript 的模块解析过程,尽管是从输入 .ts 文件开始,但反映了 Node.js 在运行输出 .js 文件时模块解析过程将发生的情况。

另一方面,当使用打包器时,打包器通常直接处理原始 .ts 文件,并在未转换的 import 语句上运行其模块解析过程。在这种情况下,考虑 TypeScript 如何发出 import 没有多大意义,因为 TypeScript 根本没有用于发出任何内容。就打包器而言,import 就是 importrequire 就是 require,因此用于解析 package.json "exports""imports" 的条件由输入 .ts 文件中看到的语法决定。同样,TypeScript 的模块解析过程在 --moduleResolution bundler 中使用的条件也由输入 TypeScript 文件中的输入语法决定——只是 require 调用目前根本没有解析。

ts
// Some library file:
declare function require(module: string): any;
// index.ts
import { foo } from "pkg"; // Resolved with "import" condition
import pkg2 = require("pkg"); // Not allowed
const pkg = require("pkg"); // Not an error, but not resolved to anything
// ^? any

由于 TypeScript 目前不支持在 --moduleResolution bundler 中解析 require 调用,因此它确实解析的所有内容都使用 "import" 条件。

隐含和强制选项

  • --moduleResolution bundler 必须与 --module esnext 选项配对。
  • --moduleResolution bundler 意味着 --allowSyntheticDefaultImports

支持的功能

node10(以前称为 node

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

支持的功能

classic

不要使用 classic

TypeScript 文档是一个开源项目。帮助我们改进这些页面 通过发送拉取请求

此页面的贡献者
ABAndrew Branch (6)
HCDCHenrique Carvalho da Cruz (1)

上次更新:2024 年 3 月 21 日