模块语法
🌐 Module syntax
TypeScript 编译器可以识别 TypeScript 和 JavaScript 文件中的标准 ECMAScript 模块语法,以及 JavaScript 文件中的多种 CommonJS 语法。
🌐 The TypeScript compiler recognizes standard ECMAScript module syntax in TypeScript and JavaScript files and many forms of CommonJS syntax in JavaScript files.
还有一些 TypeScript 特定的语法扩展可以在 TypeScript 文件和/或 JSDoc 注释中使用。
🌐 There are also a few TypeScript-specific syntax extensions that can be used in TypeScript files and/or JSDoc comments.
导入和导出特定于 TypeScript 的声明
🌐 Importing and exporting TypeScript-specific declarations
类型别名、接口、枚举和命名空间可以使用 export 修饰符从模块中导出,就像任何标准的 JavaScript 声明一样:
🌐 Type aliases, interfaces, enums, and namespaces can be exported from a module with an export modifier, like any standard JavaScript declaration:
ts// Standard JavaScript syntax...export function f() {}// ...extended to type declarationsexport type SomeType = /* ... */;export interface SomeInterface { /* ... */ }
它们也可以在命名导出中引用,甚至与标准 JavaScript 声明的引用一起引用:
🌐 They can also be referenced in named exports, even alongside references to standard JavaScript declarations:
tsexport { f, SomeType, SomeInterface };
导出的类型(以及其他 TypeScript 特定的声明)可以通过标准 ECMAScript 导入来导入:
🌐 Exported types (and other TypeScript-specific declarations) can be imported with standard ECMAScript imports:
tsimport { f, SomeType, SomeInterface } from "./module.js";
使用命名空间导入或导出时,在类型位置引用时导出的类型在命名空间上可用:
🌐 When using namespace imports or exports, exported types are available on the namespace when referenced in a type position:
tsimport * as mod from "./module.js";mod.f();mod.SomeType; // Property 'SomeType' does not exist on type 'typeof import("./module.js")'let x: mod.SomeType; // Ok
仅类型导入和导出
🌐 Type-only imports and exports
在向 JavaScript 发出导入和导出时,默认情况下,TypeScript 会自动省略(不发出)仅在类型位置使用的导入和仅引用类型的导出。仅类型的导入和导出可以用来强制这种行为,并明确显示省略。使用 import type 编写的导入声明、使用 export type { ... } 编写的导出声明,以及带有 type 关键字前缀的导入或导出指定符,都保证会从输出的 JavaScript 中被省略。
🌐 When emitting imports and exports to JavaScript, by default, TypeScript automatically elides (does not emit) imports that are only used in type positions and exports that only refer to types. Type-only imports and exports can be used to force this behavior and make the elision explicit. Import declarations written with import type, export declarations written with export type { ... }, and import or export specifiers prefixed with the type keyword are all guaranteed to be elided from the output JavaScript.
ts// @Filename: main.tsimport { f, type SomeInterface } from "./module.js";import type { SomeType } from "./module.js";class C implements SomeInterface {constructor(p: SomeType) {f();}}export type { C };// @Filename: main.jsimport { f } from "./module.js";class C {constructor(p) {f();}}
偶数值可以使用 import type 导入,但由于它们不会出现在输出的 JavaScript 中,因此只能在非输出位置使用:
🌐 Even values can be imported with import type, but since they won’t exist in the output JavaScript, they can only be used in non-emitting positions:
tsimport type { f } from "./module.js";f(); // 'f' cannot be used as a value because it was imported using 'import type'let otherFunction: typeof f = () => {}; // Ok
仅类型导入声明不能同时声明默认导入和命名绑定,因为无法明确 type 是应用于默认导入还是整个导入声明。相反,应将导入声明拆分为两个,或者将 default 用作命名绑定:
🌐 A type-only import declaration may not declare both a default import and named bindings, since it appears ambiguous whether type applies to the default import or to the entire import declaration. Instead, split the import declaration into two, or use default as a named binding:
tsimport type fs, { BigIntOptions } from "fs";// ^^^^^^^^^^^^^^^^^^^^^// Error: A type-only import can specify a default import or named bindings, but not both.import type { default as fs, BigIntOptions } from "fs"; // Ok
import() 类型
🌐 import() types
TypeScript 提供了一种类型语法,类似于 JavaScript 的动态 import,用于在不编写导入声明的情况下引用模块的类型:
🌐 TypeScript provides a type syntax similar to JavaScript’s dynamic import for referencing the type of a module without writing an import declaration:
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 注释中特别有用,否则无法导入类型:
🌐 This is especially useful in JSDoc comments in JavaScript files, where it’s not possible to import types otherwise:
ts/** @type {import("webpack").Configuration} */module.exports = {// ...}
export = 和 import = require()
🌐 export = and import = require()
在输出 CommonJS 模块时,TypeScript 文件可以使用 module.exports = ... 和 const mod = require("...") JavaScript 语法的直接对应形式:
🌐 When emitting CommonJS modules, TypeScript files can use a direct analog of module.exports = ... and const mod = require("...") JavaScript syntax:
ts// @Filename: main.tsimport fs = require("fs");export = fs.readFileSync("...");// @Filename: main.js"use strict";const fs = require("fs");module.exports = fs.readFileSync("...");
此语法优于 JavaScript 语法,因为变量声明和属性赋值不能引用 TypeScript 类型,而特殊的 TypeScript 语法可以:
🌐 This syntax was used over its JavaScript counterparts since variable declarations and property assignments could not refer to TypeScript types, whereas special TypeScript syntax could:
ts// @Filename: a.tsinterface Options { /* ... */ }module.exports = Options; // Error: 'Options' only refers to a type, but is being used as a value here.export = Options; // Ok// @Filename: b.tsconst Options = require("./a");const options: Options = { /* ... */ }; // Error: 'Options' refers to a value, but is being used as a type here.// @Filename: c.tsimport Options = require("./a");const options: Options = { /* ... */ }; // Ok
环境模块
🌐 Ambient modules
TypeScript 在脚本(非模块)文件中支持一种语法,用于声明在运行时存在但没有对应文件的模块。这些_环境模块_通常表示运行时提供的模块,例如 Node.js 中的 "fs" 或 "path":
🌐 TypeScript supports a syntax in script (non-module) files for declaring a module that exists in the runtime but has no corresponding file. These ambient modules usually represent runtime-provided modules, like "fs" or "path" in Node.js:
tsdeclare module "path" {export function normalize(p: string): string;export function join(...paths: any[]): string;export var sep: string;}
一旦环境模块加载到 TypeScript 程序中,TypeScript 将识别其他文件中声明的模块的导入:
🌐 Once an ambient module is loaded into a TypeScript program, TypeScript will recognize imports of the declared module in other files:
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 的影响):
🌐 Ambient module declarations are easy to confuse with module augmentations since they use identical syntax. This module declaration syntax becomes a module augmentation when the file is a module, meaning it has a top-level import or export statement (or is affected by --moduleDetection force or auto):
ts// Not an ambient module declaration anymore!export {};declare module "path" {export function normalize(p: string): string;export function join(...paths: any[]): string;export var sep: string;}
环境模块可以在模块声明主体内使用导入来引用其他模块,而无需将包含文件转换为模块(这将使环境模块声明成为模块增强):
🌐 Ambient modules may use imports inside the module declaration body to refer to other modules without turning the containing file into a module (which would make the ambient module declaration a module augmentation):
tsdeclare module "m" {// Moving this outside "m" would totally change the meaning of the file!import { SomeType } from "other";export function f(): SomeType;}
一个 pattern 环境模块在其名称中包含一个 * 通配符字符,用于匹配导入路径中的零个或多个字符。这在声明由自定义加载器提供的模块时非常有用:
🌐 A pattern ambient module contains a single * wildcard character in its name, matching zero or more characters in import paths. This can be useful for declaring modules provided by custom loaders:
tsdeclare module "*.html" {const content: string;export default content;}
module 编译器选项
🌐 The module compiler option
本节讨论每个 module 编译器选项值的详细信息。有关该选项的具体作用及其在整体编译过程中的位置,请参阅 模块输出格式 理论部分。简而言之,module 编译器选项历史上仅用于控制生成的 JavaScript 文件的输出模块格式。然而,较新的 node16、node18 和 nodenext 值描述了 Node.js 模块系统的广泛特性,包括支持的模块格式、每个文件的模块格式如何确定以及不同模块格式如何互操作。
🌐 This section discusses the details of each module compiler option value. See the Module output format theory section for more background on what the option is and how it fits into the overall compilation process. In brief, the module compiler option was historically only used to control the output module format of emitted JavaScript files. The more recent node16, node18, and nodenext values, however, describe a wide range of characteristics of Node.js’s module system, including what module formats are supported, how the module format of each file is determined, and how different module formats interoperate.
node16、node18、node20、nodenext
🌐 node16, node18, node20, nodenext
Node.js 支持 CommonJS 和 ECMAScript 模块,两者各有特定规则规定每个文件可以采用的格式以及两种格式如何相互操作。node16、node18 和 nodenext 描述了 Node.js 双格式模块系统的完整行为范围,并且可以以 CommonJS 或 ESM 格式生成文件。这与其他所有 module 选项不同,后者与运行时无关,并强制将所有输出文件生成单一格式,由用户自行确保输出对于其运行时有效。
🌐 Node.js supports both CommonJS and ECMAScript modules, with specific rules for which format each file can be and how the two formats are allowed to interoperate. node16, node18, and nodenext describe the full range of behavior for Node.js’s dual-format module system, and emit files in either CommonJS or ESM format. This is different from every other module option, which are runtime-agnostic and force all output files into a single format, leaving it to the user to ensure the output is valid for their runtime.
一个常见的误解是
node16——nodenext只会输出 ES 模块。实际上,这些模式描述的是 支持 ES 模块的 Node.js 版本,而不仅仅是 使用 ES 模块的项目。根据每个文件的检测到的模块格式,同时支持 ESM 和 CommonJS 输出。由于它们是唯一反映 Node.js 双模块系统复杂性的module选项,因此它们是所有打算在 Node.js v12 或更高版本中运行的应用和库的 唯一正确的module选项,无论是否使用 ES 模块。
固定版本的 node16 和 node18 模式表示在各自 Node.js 版本中已稳定的模块系统行为,而 nodenext 模式会随着 Node.js 的最新稳定版本而变化。下表总结了三种模式之间的当前差异:
🌐 The fixed-version node16 and node18 modes represent the module system behavior stabilized in their respective Node.js versions, while the nodenext mode changes with the latest stable versions of Node.js. The following table summarizes the current differences between the three modes:
target |
moduleResolution |
导入断言 | 导入属性 | JSON 导入 | require(esm) | |
|---|---|---|---|---|---|---|
| node16 | es2022 |
node16 |
❌ | ❌ | 无限制 | ❌ |
| node18 | es2022 |
node16 |
✅ | ✅ | 需要 type "json" |
❌ |
| nodenext | esnext |
nodenext |
❌ | ✅ | 需要 type "json" |
✅ |
模块格式检测
🌐 Module format detection
.mts/.mjs/.d.mts文件始终是 ES 模块。.cts/.cjs/.d.cts文件总是 CommonJS 模块。.ts/.tsx/.js/.jsx/.d.ts文件是 ES 模块,如果最近的上级 package.json 文件包含"type": "module",否则为 CommonJS 模块。
输入 .ts/.tsx/.mts/.cts 文件的检测到的模块格式决定了生成的 JavaScript 文件的模块格式。例如,一个完全由 .ts 文件组成的项目,在 --module nodenext 下默认会生成所有 CommonJS 模块,并且可以通过在项目的 package.json 中添加 "type": "module" 来生成所有 ES 模块。
🌐 The detected module format of input .ts/.tsx/.mts/.cts files determines the module format of the emitted JavaScript files. So, for example, a project consisting entirely of .ts files will emit all CommonJS modules by default under --module nodenext, and can be made to emit all ES modules by adding "type": "module" to the project package.json.
互操作性规则
🌐 Interoperability rules
- 当一个 ES 模块引用一个 CommonJS 模块时:
- 当 CommonJS 模块引用 ES 模块时:
- 在
node16和node18中,require不能引用 ES 模块。对于 TypeScript,这包括在被检测为 CommonJS 模块的文件中的import语句,因为这些import语句会在生成的 JavaScript 中被转换为require调用。 - 在“nodenext”中,为了反映Node.js 22.12.0及以后版本的行为,“require”可以引用ES模块。在Node.js中,如果ES模块或其任何导入模块使用顶层“await”,就会报错。TypeScript 不会尝试检测此情况,也不会发出编译时错误。“require”调用的结果是该模块的模块命名空间对象,即与同一模块的“await import()”结果相同(但无需“await”任何东西)。
- 动态的
import()调用总是可以用来导入一个 ES 模块。它返回一个模块命名空间对象的 Promise(就像你从另一个 ES 模块的import * as ns from "./module.js"得到的一样)。
- 在
触发
🌐 Emit
每个文件的输出格式由每个文件的检测到的模块格式决定。ESM 输出类似于--module esnext,但对 import x = require("...") 有特殊的转换,而在 --module esnext 中不允许使用:
🌐 The emit format of each file is determined by the detected module format of each file. ESM emit is similar to --module esnext, but has a special transformation for import x = require("..."), which is not allowed in --module esnext:
ts// @Filename: main.tsimport x = require("mod");
js// @Filename: main.jsimport { createRequire as _createRequire } from "module";const __require = _createRequire(import.meta.url);const x = __require("mod");
CommonJS 的输出类似于 --module commonjs,但动态 import() 调用不会被转换。这里显示启用 esModuleInterop 的输出效果:
🌐 CommonJS emit is similar to --module commonjs, but dynamic import() calls are not transformed. Emit here is shown with esModuleInterop enabled:
ts// @Filename: main.tsimport fs from "fs"; // transformedconst dynamic = import("mod"); // not transformed
js// @Filename: main.js"use strict";var __importDefault = (this && this.__importDefault) || function (mod) {return (mod && mod.__esModule) ? mod : { "default": mod };};Object.defineProperty(exports, "__esModule", { value: true });const fs_1 = __importDefault(require("fs")); // transformedconst dynamic = import("mod"); // not transformed
隐式和强制选项
🌐 Implied and enforced options
--module nodenext意味着并强制执行--moduleResolution nodenext。--module node18或node16表示并强制执行--moduleResolution node16。--module nodenext意味着--target esnext。--module node18或node16意味着--target es2022。--module nodenext或node18或node16意味着--esModuleInterop。
概括
🌐 Summary
node16、node18和nodenext是所有打算在 Node.js v12 或更高版本中运行的应用和库的唯一正确的module选项,无论它们是否使用 ES 模块。node16、node18和nodenext会根据每个文件的检测到的模块格式,以 CommonJS 或 ESM 格式输出文件。- Node.js ESM 和 CJS 之间的互操作规则体现在类型检查中。
- ESM emit 将
import x = require("...")转换为由createRequire导入构建的require调用。 - CommonJS emit 会保持动态
import()调用不被转换,因此 CommonJS 模块可以异步导入 ES 模块。
preserve
在 --module preserve(在 TypeScript 5.4 中新增)中,输入文件中编写的 ECMAScript 导入和导出会在输出中保留,而 CommonJS 风格的 import x = require("...") 和 export = ... 语句会作为 CommonJS 的 require 和 module.exports 输出。换句话说,每个单独的导入或导出语句的格式都会被保留,而不会被强制转换为整个编译(甚至整个文件)的单一格式。
🌐 In --module preserve (added in TypeScript 5.4), ECMAScript imports and exports written in input files are preserved in the output, and CommonJS-style import x = require("...") and export = ... statements are emitted as CommonJS require and module.exports. In other words, the format of each individual import or export statement is preserved, rather than being coerced into a single format for the whole compilation (or even a whole file).
虽然在同一个文件中同时使用 import 和 require 调用的情况很少,但这种 module 模式最能反映大多数现代打包工具以及 Bun 运行时的功能。
🌐 While it’s rare to need to mix imports and require calls in the same file, this module mode best reflects the capabilities of most modern bundlers, as well as the Bun runtime.
为什么要关心 TypeScript 在使用打包工具或 Bun 时的
module输出,同时你可能还设置了noEmit?TypeScript 的类型检查和模块解析行为会受到它 将要 输出的模块格式的影响。设置module可以让 TypeScript 了解你的打包工具或运行时将如何处理导入和导出,从而保证你在导入值上看到的类型与运行时或打包后的实际情况一致。更多讨论请参见--moduleResolution bundler。
示例
🌐 Examples
ts// @Filename: main.tsimport x, { y, z } from "mod";import mod = require("mod");const dynamic = import("mod");export const e1 = 0;export default "default export";
js// @Filename: main.jsimport x, { y, z } from "mod";const mod = require("mod");const dynamic = import("mod");export const e1 = 0;export default "default export";
隐式和强制选项
🌐 Implied and enforced options
--module preserve意味着--moduleResolution bundler。--module preserve意味着--esModuleInterop。
选项
--esModuleInterop在--module preserve中默认启用,仅用于其类型检查行为。由于在--module preserve中导入从不转换为 require 调用,--esModuleInterop不会影响生成的 JavaScript。
es2015、es2020、es2022、esnext
🌐 es2015, es2020, es2022, esnext
概括
🌐 Summary
- 在打包工具、Bun 和 tsx 中使用
esnext与--moduleResolution bundler。 - 不要用于 Node.js。在 package.json 中使用
node16、node18或nodenext配合"type": "module"来为 Node.js 输出 ES 模块。 import mod = require("mod")在非声明文件中不允许使用。es2020增加了对import.meta属性的支持。es2022增加了对顶层await的支持。esnext是一个动态变化的目标,可能包括对 ECMAScript 模块阶段 3 提案的支持。- 触发的文件是 ES 模块,但依赖可以是任何格式。
示例
🌐 Examples
ts// @Filename: main.tsimport x, { y, z } from "mod";import * as mod from "mod";const dynamic = import("mod");console.log(x, y, z, mod, dynamic);export const e1 = 0;export default "default export";
js// @Filename: main.jsimport x, { y, z } from "mod";import * as mod from "mod";const dynamic = import("mod");console.log(x, y, z, mod, dynamic);export const e1 = 0;export default "default export";
commonjs
概括
🌐 Summary
- 你可能不应该使用这个。请使用
node16、node18或nodenext来为 Node.js 生成 CommonJS 模块。 - 触发的文件是 CommonJS 模块,但依赖可以是任何格式。
- 动态
import()被转换为require()调用的 Promise。 esModuleInterop会影响默认导入和命名空间导入的输出代码。
示例
🌐 Examples
输出显示为
esModuleInterop: false。
ts// @Filename: main.tsimport x, { y, z } from "mod";import * as mod from "mod";const dynamic = import("mod");console.log(x, y, z, mod, dynamic);export const e1 = 0;export default "default export";
js// @Filename: main.js"use strict";Object.defineProperty(exports, "__esModule", { value: true });exports.e1 = void 0;const mod_1 = require("mod");const mod = require("mod");const dynamic = Promise.resolve().then(() => require("mod"));console.log(mod_1.default, mod_1.y, mod_1.z, mod);exports.e1 = 0;exports.default = "default export";
ts// @Filename: main.tsimport mod = require("mod");console.log(mod);export = {p1: true,p2: false};
js// @Filename: main.js"use strict";const mod = require("mod");console.log(mod);module.exports = {p1: true,p2: false};
system
概括
🌐 Summary
- 设计用于与 SystemJS 模块加载器 一起使用。
示例
🌐 Examples
ts// @Filename: main.tsimport x, { y, z } from "mod";import * as mod from "mod";const dynamic = import("mod");console.log(x, y, z, mod, dynamic);export const e1 = 0;export default "default export";
js// @Filename: main.jsSystem.register(["mod"], function (exports_1, context_1) {"use strict";var mod_1, mod, dynamic, e1;var __moduleName = context_1 && context_1.id;return {setters: [function (mod_1_1) {mod_1 = mod_1_1;mod = mod_1_1;}],execute: function () {dynamic = context_1.import("mod");console.log(mod_1.default, mod_1.y, mod_1.z, mod, dynamic);exports_1("e1", e1 = 0);exports_1("default", "default export");}};});
amd
概括
🌐 Summary
- 专为 RequireJS 等 AMD 加载器而设计。
- 你可能不应该使用这个。改用打包工具吧。
- 触发的文件是 AMD 模块,但依赖可以是任何格式。
- 支持
outFile。
示例
🌐 Examples
ts// @Filename: main.tsimport x, { y, z } from "mod";import * as mod from "mod";const dynamic = import("mod");console.log(x, y, z, mod, dynamic);export const e1 = 0;export default "default export";
js// @Filename: main.jsdefine(["require", "exports", "mod", "mod"], function (require, exports, mod_1, mod) {"use strict";Object.defineProperty(exports, "__esModule", { value: true });exports.e1 = void 0;const dynamic = new Promise((resolve_1, reject_1) => { require(["mod"], resolve_1, reject_1); });console.log(mod_1.default, mod_1.y, mod_1.z, mod, dynamic);exports.e1 = 0;exports.default = "default export";});
umd
概括
🌐 Summary
- 专为 AMD 或 CommonJS 加载器设计。
- 不像大多数其他 UMD 封装器那样公开全局变量。
- 你可能不应该使用这个。改用打包工具吧。
- 触发的文件是 UMD 模块,但依赖可以是任何格式。
示例
🌐 Examples
ts// @Filename: main.tsimport x, { y, z } from "mod";import * as mod from "mod";const dynamic = import("mod");console.log(x, y, z, mod, dynamic);export const e1 = 0;export default "default export";
js// @Filename: main.js(function (factory) {if (typeof module === "object" && typeof module.exports === "object") {var v = factory(require, exports);if (v !== undefined) module.exports = v;}else if (typeof define === "function" && define.amd) {define(["require", "exports", "mod", "mod"], factory);}})(function (require, exports) {"use strict";var __syncRequire = typeof module === "object" && typeof module.exports === "object";Object.defineProperty(exports, "__esModule", { value: true });exports.e1 = void 0;const mod_1 = require("mod");const mod = require("mod");const dynamic = __syncRequire ? Promise.resolve().then(() => require("mod")) : new Promise((resolve_1, reject_1) => { require(["mod"], resolve_1, reject_1); });console.log(mod_1.default, mod_1.y, mod_1.z, mod, dynamic);exports.e1 = 0;exports.default = "default export";});
moduleResolution 编译器选项
🌐 The moduleResolution compiler option
本节介绍了多个 moduleResolution 模式共享的模块解析功能和过程,然后详细说明了每种模式的细节。有关该选项的更多背景信息以及它如何融入整体编译过程,请参阅 模块解析 理论部分。简而言之,moduleResolution 控制 TypeScript 如何将 模块说明符(import/export/require 语句中的字符串字面量)解析为磁盘上的文件,应设置为与目标运行时或打包器使用的模块解析器相匹配。
🌐 This section describes module resolution features and processes shared by multiple moduleResolution modes, then specifies the details of each mode. See the Module resolution theory section for more background on what the option is and how it fits into the overall compilation process. In brief, moduleResolution controls how TypeScript resolves module specifiers (string literals in import/export/require statements) to files on disk, and should be set to match the module resolver used by the target runtime or bundler.
共同特性和流程
🌐 Common features and processes
文件扩展名替换
🌐 File extension substitution
TypeScript 总是希望在内部解析到可以提供类型信息的文件,同时确保运行时或打包工具可以使用相同的路径解析到提供 JavaScript 实现的文件。对于任何根据指定的 moduleResolution 算法会在运行时或打包工具中触发 JavaScript 文件查找的模块说明符,TypeScript 会首先尝试找到具有相同名称和类似文件扩展名的 TypeScript 实现文件或类型声明文件。
🌐 TypeScript always wants to resolve internally to a file that can provide type information, while ensuring that the runtime or bundler can use the same path to resolve to a file that provides a JavaScript implementation. For any module specifier that would, according to the moduleResolution algorithm specified, trigger a lookup of a JavaScript file in the runtime or bundler, TypeScript will first try to find a TypeScript implementation file or type declaration file with the same name and analagous file extension.
| 运行时查找 | 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 文件:
🌐 Note that this behavior is independent of the actual module specifier written in the import. This means that TypeScript can resolve to a .ts or .d.ts file even if the module specifier explicitly uses a .js file extension:
tsimport x from "./mod.js";// Runtime lookup: "./mod.js"// TypeScript lookup #1: "./mod.ts"// TypeScript lookup #2: "./mod.d.ts"// TypeScript lookup #3: "./mod.js"
请参阅 TypeScript 模仿宿主的模块解析,但带有类型 了解 TypeScript 的模块解析为什么会这样工作的解释。
🌐 See TypeScript imitates the host’s module resolution, but with types for an explanation of why TypeScript’s module resolution works this way.
相对文件路径解析
🌐 Relative file path resolution
TypeScript 的所有 moduleResolution 算法都支持通过包含文件扩展名的相对路径来引用模块(根据上面的规则进行替换):
🌐 All of TypeScript’s moduleResolution algorithms support referencing a module by a relative path that includes a file extension (which will be substituted according to the rules above):
ts// @Filename: a.tsexport {};// @Filename: b.tsimport {} from "./a.js"; // ✅ Works in every `moduleResolution`
无扩展相对路径
🌐 Extensionless relative paths
在某些情况下,运行时或打包工具允许从相对路径中省略 .js 文件扩展名。TypeScript 支持这种行为,只要 moduleResolution 设置和上下文表明运行时或打包工具支持它:
🌐 In some cases, the runtime or bundler allows omitting a .js file extension from a relative path. TypeScript supports this behavior where the moduleResolution setting and the context indicate that the runtime or bundler supports it:
ts// @Filename: a.tsexport {};// @Filename: b.tsimport {} from "./a";
如果 TypeScript 判定在运行时会根据模块说明符 "./a" 检索 ./a.js,那么 ./a.js 将进行扩展名替换,并在此示例中解析为文件 a.ts。
🌐 If TypeScript determines that the runtime will perform a lookup for ./a.js given the module specifier "./a", then ./a.js will undergo extension substitution, and resolve to the file a.ts in this example.
在 Node.js 中,import 路径不支持无扩展名的相对路径,并且在 package.json 文件中指定的文件路径中也不总是支持。TypeScript 目前从不支持省略 .mjs/.mts 或 .cjs/.cts 文件扩展名,尽管某些运行时和打包工具支持这样做。
🌐 Extensionless relative paths are not supported in import paths in Node.js, and are not always supported in file paths specified in package.json files. TypeScript currently never supports omitting a .mjs/.mts or .cjs/.cts file extension, even though some runtimes and bundlers do.
目录模块(索引文件解析)
🌐 Directory modules (index file resolution)
在某些情况下,可以将目录而不是文件作为模块引用。在最简单和最常见的情况下,这涉及运行时或打包工具在目录中查找一个 index.js 文件。TypeScript 支持这种行为,当 moduleResolution 设置和上下文表明运行时或打包工具支持它时:
🌐 In some cases, a directory, rather than a file, can be referenced as a module. In the simplest and most common case, this involves the runtime or bundler looking for an index.js file in a directory. TypeScript supports this behavior where the moduleResolution setting and the context indicate that the runtime or bundler supports it:
ts// @Filename: dir/index.tsexport {};// @Filename: b.tsimport {} from "./dir";
如果 TypeScript 判定在运行时会根据模块说明符 "./dir" 检索 ./dir/index.js,那么 ./dir/index.js 将进行扩展名替换,并在此示例中解析为文件 dir/index.ts。
🌐 If TypeScript determines that the runtime will perform a lookup for ./dir/index.js given the module specifier "./dir", then ./dir/index.js will undergo extension substitution, and resolve to the file dir/index.ts in this example.
目录模块也可以包含一个 package.json 文件,其中支持解析 "main" 和 "types" 字段,并且优先于 index.js 查找。目录模块中也支持 "typesVersions" 字段。
🌐 Directory modules may also contain a package.json file, where resolution of the "main" and "types" fields are supported, and take precedence over index.js lookups. The "typesVersions" field is also supported in directory modules.
请注意,目录模块不同于 node_modules 包,仅支持包可用功能的子集,并且在某些情况下根本不受支持。Node.js 将它们视为旧功能。
🌐 Note that directory modules are not the same as node_modules packages and only support a subset of the features available to packages, and are not supported at all in some contexts. Node.js considers them a legacy feature.
paths
概述
🌐 Overview
TypeScript 提供了一种通过 paths 编译器选项覆盖编译器对裸模块标识符的模块解析的方法。虽然该功能最初是为与 AMD 模块加载器一起使用而设计的(在 ESM 出现或打包工具被广泛使用之前,这是一种在浏览器中运行模块的方式),但在今天,当运行时或打包工具支持 TypeScript 未建模的模块解析功能时,它仍然有用。例如,在使用 --experimental-network-imports 运行 Node.js 时,你可以为特定的 https:// 导入手动指定本地类型定义文件:
🌐 TypeScript offers a way to override the compiler’s module resolution for bare specifiers with the paths compiler option. While the feature was originally designed to be used with the AMD module loader (a means of running modules in the browser before ESM existed or bundlers were widely used), it still has uses today when a runtime or bundler supports module resolution features that TypeScript does not model. For example, when running Node.js with --experimental-network-imports, you can manually specify a local type definition file for a specific https:// import:
json{"compilerOptions": {"module": "nodenext","paths": {"https://esm.sh/lodash@4.17.21": ["./node_modules/@types/lodash/index.d.ts"]}}}
ts// Typed by ./node_modules/@types/lodash/index.d.ts due to `paths` entryimport { add } from "https://esm.sh/lodash@4.17.21";
对于使用打包工具构建的应用来说,在打包配置中定义便利的路径别名,然后通过 paths 告诉 TypeScript 这些别名,也是很常见的做法:
🌐 It’s also common for apps built with bundlers to define convenience path aliases in their bundler configuration, and then inform TypeScript of those aliases with paths:
json{"compilerOptions": {"module": "esnext","moduleResolution": "bundler","paths": {"@app/*": ["./src/*"]}}}
paths 不会影响触发
🌐 paths does not affect emit
paths 选项不会更改 TypeScript 输出代码中的导入路径。因此,很容易创建在 TypeScript 中看起来可行但在运行时会崩溃的路径别名:
🌐 The paths option does not change the import path in the code emitted by TypeScript. Consequently, it’s very easy to create path aliases that appear to work in TypeScript but will crash at runtime:
json{"compilerOptions": {"module": "nodenext","paths": {"node-has-no-idea-what-this-is": ["./oops.ts"]}}}
ts// TypeScript: ✅// Node.js: 💥import {} from "node-has-no-idea-what-this-is";
虽然打包的应用设置 paths 是可以的,但发布的库绝对不应该这样做,因为生成的 JavaScript 如果用户没有在 TypeScript 和他们的打包工具中设置相同的别名,将无法工作。库和应用都可以考虑将 package.json "imports" 作为方便的 paths 别名的标准替代方案。
🌐 While it’s ok for bundled apps to set up paths, it’s very important that published libraries do not, since the emitted JavaScript will not work for consumers of the library without those users setting up the same aliases for both TypeScript and their bundler. Both libraries and apps can consider package.json "imports" as a standard replacement for convenience paths aliases.
paths 不应指向 monorepo 包或 node_modules 包
🌐 paths should not point to monorepo packages or node_modules packages
当模块说明符匹配 paths 别名时,它们是裸说明符,但一旦别名被解析,模块解析将按照解析后的路径作为相对路径进行。因此,对于 node_modules 包查找 会发生的解析特性,包括 package.json 中 "exports" 字段的支持,在匹配 paths 别名时不会生效。如果使用 paths 指向 node_modules 包,可能会导致意外的行为:
🌐 While module specifiers that match paths aliases are bare specifiers, once the alias is resolved, module resolution proceeds on the resolved path as a relative path. Consequently, resolution features that happen for node_modules package lookups, including package.json "exports" field support, do not take effect when a paths alias is matched. This can lead to surprising behavior if paths is used to point to a node_modules package:
ts{"compilerOptions": {"paths": {"pkg": ["./node_modules/pkg/dist/index.d.ts"],"pkg/*": ["./node_modules/pkg/*"]}}}
虽然这种配置可能会模拟一些包解析的行为,但它会覆盖包的 package.json 文件中定义的任何 main、types、exports 和 typesVersions,并且从包中导入可能在运行时失败。
🌐 While this configuration may simulate some of the behavior of package resolution, it overrides any main, types, exports, and typesVersions the package’s package.json file defines, and imports from the package may fail at runtime.
相同的注意事项也适用于在一个 monorepo 中相互引用的包。与其使用 paths 让 TypeScript 人为地将 "@my-scope/lib" 解析到一个兄弟包,不如通过 npm、yarn 或 pnpm 使用工作区将你的包符号链接到 node_modules 中,这样 TypeScript 和运行时或打包工具都能进行真实的 node_modules 包查找。如果 monorepo 中的包将发布到 npm,这一点尤其重要——因为一旦被用户安装,这些包将通过 node_modules 包查找相互引用,而使用工作区可以让你在本地开发时测试这种行为。
🌐 The same caveat applies to packages referencing each other in a monorepo. Instead of using paths to make TypeScript artificially resolve "@my-scope/lib" to a sibling package, it’s best to use workspaces via npm, yarn, or pnpm to symlink your packages into node_modules, so both TypeScript and the runtime or bundler perform real node_modules package lookups. This is especially important if the monorepo packages will be published to npm—the packages will reference each other via node_modules package lookups once installed by users, and using workspaces allows you to test that behavior during local development.
与 baseUrl 的关系
🌐 Relationship to baseUrl
当提供 baseUrl 时,每个 paths 数组中的值相对于 baseUrl 解析。否则,它们相对于定义它们的 tsconfig.json 文件解析。
🌐 When baseUrl is provided, the values in each paths array are resolved relative to the baseUrl. Otherwise, they are resolved relative to the tsconfig.json file that defines them.
通配符替换
🌐 Wildcard substitutions
paths 模式可以包含一个 * 通配符,它可以匹配任意字符串。然后可以在文件路径值中使用 * 标记来替换匹配的字符串:
json{"compilerOptions": {"paths": {"@app/*": ["./src/*"]}}}
在解析 "@app/components/Button" 的导入时,TypeScript 会匹配 @app/*,将 * 绑定到 components/Button,然后尝试相对于 tsconfig.json 路径解析 ./src/components/Button 的路径。其余的查找将按照 moduleResolution 设置,与任何其他相对路径查找遵循相同的规则。
🌐 When resolving an import of "@app/components/Button", TypeScript will match on @app/*, binding * to components/Button, and then attempt to resolve the path ./src/components/Button relative to the tsconfig.json path. The remainder of this lookup will follow the same rules as any other relative path lookup according to the moduleResolution setting.
当多个模式匹配一个模块说明符时,将使用在任何 * 令牌之前具有最长匹配前缀的模式:
🌐 When multiple patterns match a module specifier, the pattern with the longest matching prefix before any * token is used:
json{"compilerOptions": {"paths": {"*": ["./src/foo/one.ts"],"foo/*": ["./src/foo/two.ts"],"foo/bar": ["./src/foo/three.ts"]}}}
在解析 "foo/bar" 的导入时,所有三个 paths 模式都匹配,但使用最后一个,因为 "foo/bar" 比 "foo/" 和 "" 更长。
🌐 When resolving an import of "foo/bar", all three paths patterns match, but the last is used because "foo/bar" is longer than "foo/" and "".
回退
🌐 Fallbacks
路径映射可以提供多个文件路径。如果某个路径解析失败,将尝试数组中的下一个路径,直到解析成功或到达数组末尾。
🌐 Multiple file paths can be provided for a path mapping. If resolution fails for one path, the next one in the array will be attempted until resolution succeeds or the end of the array is reached.
json{"compilerOptions": {"paths": {"*": ["./vendor/*", "./types/*"]}}}
baseUrl
baseUrl设计用于 AMD 模块加载器。如果你没有使用 AMD 模块加载器,那么你可能不应该使用baseUrl。自 TypeScript 4.1 起,使用paths不再需要baseUrl,也不应该仅为了设置paths的值解析的目录而使用它。
baseUrl 编译器选项可以与任何 moduleResolution 模式结合使用,并指定一个目录,从该目录解析裸模块标识符(即不以 ./、../ 或 / 开头的模块标识符)。在支持 moduleResolution 模式中,baseUrl 的优先级高于 node_modules 包查找。
🌐 The baseUrl compiler option can be combined with any moduleResolution mode and specifies a directory that bare specifiers (module specifiers that don’t begin with ./, ../, or /) are resolved from. baseUrl has a higher precedence than node_modules package lookups in moduleResolution modes that support them.
在执行 baseUrl 查找时,解析过程遵循与其他相对路径解析相同的规则。例如,在支持无扩展名相对路径 的 moduleResolution 模式下,如果 baseUrl 设置为 /src,模块指定符 "some-file" 可能会解析为 /src/some-file.ts。
🌐 When performing a baseUrl lookup, resolution proceeds with the same rules as other relative path resolutions. For example, in a moduleResolution mode that supports extensionless relative paths a module specifier "some-file" may resolve to /src/some-file.ts if baseUrl is set to /src.
相对模块说明符的解析从不受 baseUrl 选项的影响。
🌐 Resolution of relative module specifiers are never affected by the baseUrl option.
node_modules 包查找
🌐 node_modules package lookups
Node.js 将不是相对路径、绝对路径或 URL 的模块说明符视为对包的引用,它会在 node_modules 子目录中查找这些包。打包工具方便地采用了这种行为,使用户能够使用与 Node.js 相同的依赖管理系统,甚至通常可以使用相同的依赖。TypeScript 的所有 moduleResolution 选项(除了 classic)都支持 node_modules 查找。(classic 当其他解析方法失败时支持在 node_modules/@types 中查找,但从不直接在 node_modules 中查找包。) 每个 node_modules 包的查找结构如下(在优先级更高的裸模块说明符规则之后开始,比如 paths、baseUrl、自名导入以及 package.json "imports" 查找已用尽):
🌐 Node.js treats module specifiers that aren’t relative paths, absolute paths, or URLs as references to packages that it looks up in node_modules subdirectories. Bundlers conveniently adopted this behavior to allow their users to use the same dependency management system, and often even the same dependencies, as they would in Node.js. All of TypeScript’s moduleResolution options except classic support node_modules lookups. (classic supports lookups in node_modules/@types when other means of resolution fail, but never looks for packages in node_modules directly.) Every node_modules package lookup has the following structure (beginning after higher precedence bare specifier rules, like paths, baseUrl, self-name imports, and package.json "imports" lookups have been exhausted):
- 对于导入文件的每个上级目录,如果其中存在
node_modules目录:- 如果在
node_modules中存在一个与包同名的目录:- 尝试从包目录解析类型。
- 如果找到结果,则返回该结果并停止搜索。
- 如果在
node_modules/@types中存在一个与包同名的目录:- 尝试从
@types包目录解析类型。 - 如果找到结果,则返回该结果并停止搜索。
- 尝试从
- 如果在
- 在所有
node_modules目录中重复之前的搜索,但这一次允许 JavaScript 文件作为结果,并且不要在@types目录中进行搜索。
所有 moduleResolution 模式(除了 classic)都遵循此模式,但它们在找到包目录后如何解析的细节有所不同,具体说明将在以下各节中介绍。
🌐 All moduleResolution modes (except classic) follow this pattern, while the details of how they resolve from a package directory, once located, differ, and are explained in the following sections.
package.json "exports"
当 moduleResolution 被设置为 node16、nodenext 或 bundler,并且 resolvePackageJsonExports 未被禁用时,TypeScript 在通过 裸标识符 node_modules 包查找 触发的包目录解析时,会遵循 Node.js 的 package.json "exports" 规范。
🌐 When moduleResolution is set to node16, nodenext, or bundler, and resolvePackageJsonExports is not disabled, TypeScript follows Node.js’s package.json "exports" spec when resolving from a package directory triggered by a bare specifier node_modules package lookup.
TypeScript 通过 "exports" 将模块说明符解析为文件路径的实现完全遵循 Node.js 的做法。然而,一旦解析出文件路径,TypeScript 仍然会尝试多种文件扩展名以优先找到类型。
🌐 TypeScript’s implementation for resolving a module specifier through "exports" to a file path follows Node.js exactly. Once a file path is resolved, however, TypeScript will still try multiple file extensions in order to prioritize finding types.
在通过 条件 "exports" 解析时,TypeScript 总是匹配 "types" 和 "default" 条件(如果存在)。此外,TypeScript 会根据 "typesVersions" 中实现的相同版本匹配规则,匹配形式为 "types@{selector}" 的版本化类型条件(其中 {selector} 是与 "typesVersions" 兼容的版本选择器)。其他不可配置的条件取决于 moduleResolution 模式,并在以下章节中说明。可以配置额外的条件以与 customConditions 编译器选项匹配。
🌐 When resolving through conditional "exports", TypeScript always matches the "types" and "default" conditions if present. Additionally, TypeScript will match a versioned types condition in the form "types@{selector}" (where {selector} is a "typesVersions"-compatible version selector) according to the same version-matching rules implemented in "typesVersions". Other non-configurable conditions are dependent on the moduleResolution mode and specified in the following sections. Additional conditions can be configured to match with the customConditions compiler option.
请注意,"exports" 的存在会阻止解析任何未在 "exports" 中明确列出或与模式匹配的子路径。
🌐 Note that the presence of "exports" prevents any subpaths not explicitly listed or matched by a pattern in "exports" from being resolved.
示例:子路径、条件和扩展替换
🌐 Example: subpaths, conditions, and extension substitution
场景:在具有以下 package.json 的包目录中,请求 "pkg/subpath" 并带有条件 ["types", "node", "require"](由 moduleResolution 设置和触发模块解析请求的上下文决定):
🌐 Scenario: "pkg/subpath" is requested with conditions ["types", "node", "require"] (determined by moduleResolution setting and the context that triggered the module resolution request) in a package directory with the following package.json:
json{"name": "pkg","exports": {".": {"import": "./index.mjs","require": "./index.cjs"},"./subpath": {"import": "./subpath/index.mjs","require": "./subpath/index.cjs"}}}
包目录内解析过程:
🌐 Resolution process within the package directory:
"exports"存在吗?是的。"exports"有"./subpath"条目吗?有。exports["./subpath"]的值是一个对象——它必须在指定条件。- 第一个条件
"import"符合此请求吗?不符合。 - 第二个条件
"require"符合此请求吗?是的。 - 路径
"./subpath/index.cjs"有被识别的 TypeScript 文件扩展名吗?没有,所以使用扩展名替代。 - 通过扩展名替换,尝试以下路径,返回第一个存在的路径,否则返回
undefined:./subpath/index.cts./subpath/index.d.cts./subpath/index.cjs
如果存在 ./subpath/index.cts 或 ./subpath.d.cts,解析就完成了。否则,解析会按照 node_modules 包查找 规则,在 node_modules/@types/pkg 和其他 node_modules 目录中搜索以解析类型。如果没有找到类型,则会对所有 node_modules 进行第二次遍历,解析为 ./subpath/index.cjs(假设其存在),这算作成功解析,但不会提供类型,导致 any 类型的导入,并在启用时产生 noImplicitAny 错误。
🌐 If ./subpath/index.cts or ./subpath.d.cts exists, resolution is complete. Otherwise, resolution searches node_modules/@types/pkg and other node_modules directories in an attempt to resolve types, according to the node_modules package lookups rules. If no types are found, a second pass through all node_modules resolves to ./subpath/index.cjs (assuming it exists), which counts as a successful resolution, but one that does not provide types, leading to any-typed imports and a noImplicitAny error if enabled.
示例:显式 "types" 条件
🌐 Example: explicit "types" condition
场景:在具有以下 package.json 的包目录中,请求 "pkg/subpath" 并带有条件 ["types", "node", "import"](由 moduleResolution 设置和触发模块解析请求的上下文确定)
🌐 Scenario: "pkg/subpath" is requested with conditions ["types", "node", "import"] (determined by moduleResolution setting and the context that triggered the module resolution request) in a package directory with the following 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"}}}}
包目录内解析过程:
🌐 Resolution process within the package directory:
"exports"存在吗?是的。"exports"有"./subpath"条目吗?有。exports["./subpath"]的值是一个对象——它必须在指定条件。- 第一个条件
"import"符合此请求吗?是的。 exports["./subpath"].import的值是一个对象——它必须在指定条件。- 第一个条件
"types"符合此请求吗?是的。 - 路径
"./types/subpath/index.d.mts"是否有公认的 TypeScript 文件扩展名?是的,所以不要使用扩展名替换。 - 如果文件存在,则返回路径
"./types/subpath/index.d.mts",否则返回undefined。
示例:版本化 "types" 条件
🌐 Example: versioned "types" condition
场景:使用 TypeScript 4.7.5,在包目录中请求 "pkg/subpath",其条件为 ["types", "node", "import"](由 moduleResolution 设置和触发模块解析请求的上下文决定),该目录包含以下 package.json:
🌐 Scenario: using TypeScript 4.7.5, "pkg/subpath" is requested with conditions ["types", "node", "import"] (determined by moduleResolution setting and the context that triggered the module resolution request) in a package directory with the following 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"}}}
包目录内解析过程:
🌐 Resolution process within the package directory:
"exports"存在吗?是的。"exports"有"./subpath"条目吗?有。exports["./subpath"]的值是一个对象——它必须在指定条件。- 第一个条件
"types@>=5.2"是否符合此请求?不,4.7.5 不大于或等于 5.2。 - 第二个条件
"types@>=4.6"是否符合此请求?是的,4.7.5 大于或等于 4.6。 - 路径
"./ts4.6/subpath/index.d.ts"是否有公认的 TypeScript 文件扩展名?是的,所以不要使用扩展名替换。 - 如果文件存在,则返回路径
"./ts4.6/subpath/index.d.ts",否则返回undefined。
示例:子路径模式
🌐 Example: subpath patterns
场景:在具有以下 package.json 的包目录中,请求 "pkg/wildcard.js" 并带有条件 ["types", "node", "import"](由 moduleResolution 设置和触发模块解析请求的上下文决定)
🌐 Scenario: "pkg/wildcard.js" is requested with conditions ["types", "node", "import"] (determined by moduleResolution setting and the context that triggered the module resolution request) in a package directory with the following package.json:
json{"name": "pkg","type": "module","exports": {"./*.js": {"types": "./types/*.d.ts","default": "./dist/*.js"}}}
包目录内解析过程:
🌐 Resolution process within the package directory:
"exports"存在吗?是的。"exports"有"./wildcard.js"条目吗?没有。- 任何包含
*的键会匹配"./wildcard.js"吗?是的,"./*.js"匹配并将wildcard设置为替换值。 exports["./*.js"]的值是一个对象——它必须在指定条件。- 第一个条件
"types"符合此请求吗?是的。 - 在
./types/*.d.ts中,将*替换为替代wildcard。./types/wildcard.d.ts - 路径
"./types/wildcard.d.ts"是否有公认的 TypeScript 文件扩展名?是的,所以不要使用扩展名替换。 - 如果文件存在,则返回路径
"./types/wildcard.d.ts",否则返回undefined。
示例:"exports" 阻止其他子路径
🌐 Example: "exports" block other subpaths
场景:在具有以下 package.json 的包目录中请求 "pkg/dist/index.js":
🌐 Scenario: "pkg/dist/index.js" is requested in a package directory with the following package.json:
json{"name": "pkg","main": "./dist/index.js","exports": "./dist/index.js"}
包目录内解析过程:
🌐 Resolution process within the package directory:
"exports"存在吗?是的。exports的值是一个字符串——它必须是指向包根目录(".")的文件路径。- 请求
"pkg/dist/index.js"是针对包根目录吗?不是,它有一个子路径dist/index.js。 - 解析失败;返回
undefined。
如果没有 "exports",请求本可以成功,但 "exports" 的存在阻止了解析任何无法通过 "exports" 匹配的子路径。
🌐 Without "exports", the request could have succeeded, but the presence of "exports" prevents resolving any subpaths that cannot be matched through "exports".
package.json "typesVersions"
一个node_modules 包或目录模块可以在其 package.json 中指定一个 "typesVersions" 字段,以根据 TypeScript 编译器版本重定向 TypeScript 的解析过程,并且对于 node_modules 包,根据正在解析的子路径进行重定向。这允许包作者在一组类型定义中包含新的 TypeScript 语法,同时提供另一组以通过像 downlevel-dts 这样的工具与旧版本 TypeScript 向后兼容。"typesVersions" 在所有 moduleResolution 模式下都受支持;然而,在读取package.json "exports" 时,该字段不会被读取。
🌐 A node_modules package or directory module may specify a "typesVersions" field in its package.json to redirect TypeScript’s resolution process according to the TypeScript compiler version, and for node_modules packages, according to the subpath being resolved. This allows package authors to include new TypeScript syntax in one set of type definitions while providing another set for backward compatibility with older TypeScript versions (through a tool like downlevel-dts). "typesVersions" is supported in all moduleResolution modes; however, the field is not read in situations when package.json "exports" are read.
示例:将所有请求重定向到子目录
🌐 Example: redirect all requests to a subdirectory
场景:一个模块使用 TypeScript 5.2 导入 "pkg",其中 node_modules/pkg/package.json 是:
🌐 Scenario: a module imports "pkg" using TypeScript 5.2, where node_modules/pkg/package.json is:
json{"name": "pkg","version": "1.0.0","types": "./index.d.ts","typesVersions": {">=3.1": {"*": ["ts3.1/*"]}}}
解决过程:
🌐 Resolution process:
- (取决于编译器选项)
"exports"是否存在?不存在。 "typesVersions"存在吗?是的。- TypeScript 版本是
>=3.1吗?是的。请记住映射"*": ["ts3.1/*"]。 - 我们是在包名之后解析子路径吗?不,只是根路径
"pkg"。 "types"存在吗?是的。"typesVersions"中的任何键与./index.d.ts匹配吗?是的,"*"匹配,并将index.d.ts设置为替代值。- 在
ts3.1/*中,将*替换为替代项./index.d.ts:ts3.1/index.d.ts。 - 路径
./ts3.1/index.d.ts是否有公认的 TypeScript 文件扩展名?是的,所以不要使用扩展名替换。 - 如果文件存在,则返回路径
./ts3.1/index.d.ts,否则返回undefined。
示例:重定向对特定文件的请求
🌐 Example: redirect requests for a specific file
场景:一个模块使用 TypeScript 3.9 导入 "pkg",其中 node_modules/pkg/package.json 是:
🌐 Scenario: a module imports "pkg" using TypeScript 3.9, where node_modules/pkg/package.json is:
json{"name": "pkg","version": "1.0.0","types": "./index.d.ts","typesVersions": {"<4.0": { "index.d.ts": ["index.v3.d.ts"] }}}
解决过程:
🌐 Resolution process:
- (取决于编译器选项)
"exports"是否存在?不存在。 "typesVersions"存在吗?是的。- TypeScript 版本是
<4.0吗?是的。请记住映射"index.d.ts": ["index.v3.d.ts"]。 - 我们是在包名之后解析子路径吗?不,只是根路径
"pkg"。 "types"存在吗?是的。"typesVersions"中有任何键与./index.d.ts匹配吗?是的,"index.d.ts"匹配。- 路径
./index.v3.d.ts是否有公认的 TypeScript 文件扩展名?是的,所以不要使用扩展名替换。 - 如果文件存在,则返回路径
./index.v3.d.ts,否则返回undefined。
package.json "main" 和 "types"
🌐 package.json "main" and "types"
如果某个目录的 package.json "exports" 字段未被读取(可能由于编译选项,或因为该字段不存在,或因为该目录被解析为 目录模块 而不是 node_modules 包),并且模块指示符在包名或包含 package.json 的目录之后没有子路径,TypeScript 将尝试按顺序从这些 package.json 字段解析,以尝试找到该包或目录的主模块:
🌐 If a directory’s package.json "exports" field is not read (either due to compiler options, or because it is not present, or because the directory is being resolved as a directory module instead of a node_modules package) and the module specifier does not have a subpath after the package name or package.json-containing directory, TypeScript will attempt to resolve from these package.json fields, in order, in an attempt to find the main module for the package or directory:
"types""typings"(旧版)"main"
在 "types" 处找到的声明文件被假定为 "main" 处实现文件的准确表示。如果 "types" 和 "typings" 不存在或无法解析,TypeScript 将读取 "main" 字段并执行 扩展替换 来查找声明文件。
🌐 The declaration file found at "types" is assumed to be an accurate representation of the implementation file found at "main". If "types" and "typings" are not present or cannot be resolved, TypeScript will read the "main" field and perform extension substitution to find a declaration file.
在将一个带类型的包发布到 npm 时,建议包含一个 "types" 字段,即使 扩展替换 或 package.json "exports" 使其不必要,因为只有当 package.json 中包含 "types" 字段时,npm 才会在包注册列表上显示 TS 图标。
🌐 When publishing a typed package to npm, it’s recommended to include a "types" field even if extension substitution or package.json "exports" make it unnecessary, because npm shows a TS icon on the package registry listing only if the package.json contains a "types" field.
包相对文件路径
🌐 Package-relative file paths
如果package.json "exports" 和 package.json "typesVersions" 都不适用,则裸包标识符的子路径将根据适用的相对路径解析规则,相对于包目录进行解析。在尊重[package.json "exports"] 的模式下,即使导入通过 "exports" 解析失败,仅因为包的 package.json 中存在 "exports" 字段,该行为也会被阻止,如上面的示例所示。另一方面,如果导入无法通过 "typesVersions" 解析,则会作为回退尝试包相对的文件路径解析。
🌐 If neither package.json "exports" nor package.json "typesVersions" apply, subpaths of a bare package specifier resolve relative to the package directory, according to applicable relative path resolution rules. In modes that respect [package.json "exports"], this behavior is blocked by the mere presence of the "exports" field in the package’s package.json, even if the import fails to resolve through "exports", as demonstrated in an example above. On the other hand, if the import fails to resolve through "typesVersions", a package-relative file path resolution is attempted as a fallback.
当支持包相对路径时,它们会按照与任何其他相对路径相同的规则解析,同时考虑 moduleResolution 模式和上下文。例如,在 --moduleResolution nodenext 中,目录模块 和 无扩展名路径 仅在 require 调用中支持,而不支持 import :
🌐 When package-relative paths are supported, they resolve under the same rules as any other relative path considering the moduleResolution mode and context. For example, in --moduleResolution nodenext, directory modules and extensionless paths are only supported in require calls, not in imports:
ts// @Filename: module.mtsimport "pkg/dist/foo"; // ❌ import, needs `.js` extensionimport "pkg/dist/foo.js"; // ✅import foo = require("pkg/dist/foo"); // ✅ require, no extension needed
package.json "imports" 和自命名导入
🌐 package.json "imports" and self-name imports
当 moduleResolution 设置为 node16、nodenext 或 bundler,且 resolvePackageJsonImports 未被禁用时,TypeScript 会尝试通过导入文件最近的祖级 package.json 的 "imports" 字段来解析以 # 开头的导入路径。类似地,当启用 package.json "exports" 查找 时,TypeScript 会尝试通过该 package.json 的 "exports" 字段解析以当前包名开头的导入路径——也就是导入文件最近的祖级 package.json 中 "name" 字段的值。这两个功能都允许包中的文件导入同一包中的其他文件,从而替代相对导入路径。
🌐 When moduleResolution is set to node16, nodenext, or bundler, and resolvePackageJsonImports is not disabled, TypeScript will attempt to resolve import paths beginning with # through the "imports" field of the nearest ancestor package.json of the importing file. Similarly, when package.json "exports" lookups are enabled, TypeScript will attempt to resolve import paths beginning with the current package name—that is, the value in the "name" field of the nearest ancestor package.json of the importing file—through the "exports" field of that package.json. Both of these features allow files in a package to import other files in the same package, replacing a relative import path.
TypeScript 在解析 "imports" 和 自引用 时完全遵循 Node.js 的解析算法,直到文件路径被解析为止。此时,TypeScript 的解析算法会根据包含被解析的 "imports" 或 "exports" 的 package.json 属于 node_modules 依赖还是本地正在编译的项目(即其目录包含包含导入文件的项目的 tsconfig.json 文件)而分支:
🌐 TypeScript follows Node.js’s resolution algorithm for "imports" and self references exactly up until a file path is resolved. At that point, TypeScript’s resolution algorithm forks based on whether the package.json containing the "imports" or "exports" being resolved belongs to a node_modules dependency or the local project being compiled (i.e., its directory contains the tsconfig.json file for the project that contains the importing file):
- 如果 package.json 位于
node_modules,TypeScript 将对文件路径应用 扩展名替换,如果它尚未具有已识别的 TypeScript 文件扩展名,并检查生成的文件路径是否存在。 - 如果 package.json 是本地项目的一部分,则会执行一个额外的重映射步骤,以查找最终将生成从
"imports"解析的输出 JavaScript 或声明文件路径的 输入 TypeScript 实现文件。没有这个步骤,任何解析"imports"路径的编译都会引用 上一次编译 的输出文件,而不是当前编译中要包含的其他输入文件。此重映射使用 tsconfig.json 中的outDir/declarationDir和rootDir,因此使用"imports"通常需要显式设置rootDir。
这种变体允许包作者编写 "imports" 和 "exports" 字段,这些字段仅引用将发布到 npm 的编译输出,同时仍允许本地开发使用原始的 TypeScript 源文件。
🌐 This variation allows package authors to write "imports" and "exports" fields that reference only the compilation outputs that will be published to npm, while still allowing local development to use the original TypeScript source files.
示例:具有条件的本地项目
🌐 Example: local project with conditions
场景:在包含 tsconfig.json 和 package.json 的项目目录中,"/src/main.mts" 在满足 ["types", "node", "import"] 条件(由 moduleResolution 设置和触发模块解析请求的上下文决定)时导入 "#utils":
🌐 Scenario: "/src/main.mts" imports "#utils" with conditions ["types", "node", "import"] (determined by moduleResolution setting and the context that triggered the module resolution request) in a project directory with a tsconfig.json and 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"}}}
解决过程:
🌐 Resolution process:
- 导入路径以
#开始,尝试通过"imports"解析。 "imports"是否存在于最近的上级 package.json 文件中?是的。"#utils"是否存在于"imports"对象中?是的。imports["#utils"]的值是一个对象——它必须在指定条件。- 第一个条件
"import"符合此请求吗?是的。 - 我们应该尝试将输出路径映射到输入路径吗?是的,因为:
- package.json 在
node_modules里吗?不,它在本地项目中。 - tsconfig.json 是否位于 package.json 目录中?是的。
- package.json 在
- 在
./dist/utils.d.mts中,将outDir前缀替换为rootDir。./src/utils.d.mts - 将输出扩展名
.d.mts替换为相应的输入扩展名.mts。./src/utils.mts - 如果文件存在,则返回路径
"./src/utils.mts"。否则,如果文件存在,则返回路径"./dist/utils.d.mts"。
示例:带子路径模式的 node_modules 依赖
🌐 Example: node_modules dependency with subpath pattern
场景:"/node_modules/pkg/main.mts" 在满足条件 ["types", "node", "import"](由 moduleResolution 设置和触发模块解析请求的上下文决定)的情况下导入 "#internal/utils",package.json 如下:
🌐 Scenario: "/node_modules/pkg/main.mts" imports "#internal/utils" with conditions ["types", "node", "import"] (determined by moduleResolution setting and the context that triggered the module resolution request) with the package.json:
json// /node_modules/pkg/package.json{"name": "pkg","imports": {"#internal/*": {"import": "./dist/internal/*.mjs","require": "./dist/internal/*.cjs"}}}
解决过程:
🌐 Resolution process:
- 导入路径以
#开始,尝试通过"imports"解析。 "imports"是否存在于最近的上级 package.json 文件中?是的。"#internal/utils"存在于"imports"对象中吗?不存在,请检查模式匹配。- 是否有任何带有
*的键匹配"#internal/utils"?是的,"#internal/*"匹配并将utils设置为替代项。 imports["#internal/*"]的值是一个对象——它必须在指定条件。- 第一个条件
"import"符合此请求吗?是的。 - 我们是否应该尝试将输出路径映射到输入路径?不,因为 package.json 位于
node_modules。 - 在
./dist/internal/*.mjs中,将*替换为替代utils。./dist/internal/utils.mjs - 路径
./dist/internal/utils.mjs是否有已认可的 TypeScript 文件扩展名?**没有,请尝试扩展名替换。**10. 通过 扩展名替换,尝试以下路径,返回第一个存在的路径,否则返回undefined:./dist/internal/utils.mts./dist/internal/utils.d.mts./dist/internal/utils.mjs
node16,nodenext
🌐 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" 条件)。
🌐 These modes reflect the module resolution behavior of Node.js v12 and later. (node16 and nodenext are currently identical, but if Node.js makes significant changes to its module system in the future, node16 will be frozen while nodenext will be updated to reflect the new behavior.) In Node.js, the resolution algorithm for ECMAScript imports is significantly different from the algorithm for CommonJS require calls. For each module specifier being resolved, the syntax and the module format of the importing file are first used to determine whether the module specifier will be in an import or require in the emitted JavaScript. That information is then passed into the module resolver to determine which resolution algorithm to use (and whether to use the "import" or "require" condition for package.json "exports" or "imports").
被确定为 CommonJS 格式 的 TypeScript 文件仍然可以默认使用
import和export语法,但生成的 JavaScript 将改用require和module.exports。这意味着常见会看到使用require算法解析的import语句。如果这带来困惑,可以启用verbatimModuleSyntax编译器选项,它会禁止使用会被生成为require调用的import语句。
请注意,动态 import() 调用总是使用 import 算法解析,这是根据 Node.js 的行为。然而,import() 类型的解析则根据导入文件的格式进行(为了向后兼容现有的 CommonJS 格式类型声明):
🌐 Note that dynamic import() calls are always resolved using the import algorithm, according to Node.js’s behavior. However, import() types are resolved according to the format of the importing file (for backward compatibility with existing CommonJS-format type declarations):
ts// @Filename: module.mtsimport x from "./mod.js"; // `import` algorithm due to file format (emitted as-written)import("./mod.js"); // `import` algorithm due to syntax (emitted as-written)type Mod = typeof import("./mod.js"); // `import` algorithm due to file formatimport mod = require("./mod"); // `require` algorithm due to syntax (emitted as `require`)// @Filename: commonjs.ctsimport x from "./mod"; // `require` algorithm due to file format (emitted as `require`)import("./mod.js"); // `import` algorithm due to syntax (emitted as-written)type Mod = typeof import("./mod"); // `require` algorithm due to file formatimport mod = require("./mod"); // `require` algorithm due to syntax (emitted as `require`)
隐式和强制选项
🌐 Implied and enforced options
--moduleResolution node16和nodenext必须与--module node16,node18,node20或nodenext配对。
支持的功能
🌐 Supported features
功能按优先顺序列出。
🌐 Features are listed in order of precedence.
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 文件中的导入语句:
🌐 It’s instructive to think about the similarities and differences between --moduleResolution bundler and --moduleResolution nodenext, particularly in how they decide what conditions to use when resolving package.json "exports" or "imports". Consider an import statement in a .ts file:
ts// index.tsimport { foo } from "pkg";
回想一下,在 --module nodenext --moduleResolution nodenext 中,--module 设置首先确定导入是作为 import 还是 require 调用输出到 .js 文件,然后将该信息传递给 TypeScript 的模块解析器,模块解析器决定是否在 "pkg" 的 package.json 中匹配 "import" 或 "require" 条件。假设此文件范围内没有 package.json 文件。文件扩展名是 .ts,因此输出文件扩展名将是 .js,Node.js 会将其解释为 CommonJS,因此 TypeScript 会将该 import 输出为 require 调用。因此,模块解析器在从 "pkg" 解析 "exports" 时将使用 require 条件。
🌐 Recall that in --module nodenext --moduleResolution nodenext, the --module setting first determines whether the import will be emitted to the .js file as an import or require call, then passes that information to TypeScript’s module resolver, which decides whether to match "import" or "require" conditions in "pkg"’s package.json "exports" accordingly. Let’s assume that there’s no package.json in scope of this file. The file extension is .ts, so the output file extension will be .js, which Node.js will interpret as CommonJS, so TypeScript will emit this import as a require call. So, the module resolver will use the require condition as it resolves "exports" from "pkg".
在 --moduleResolution bundler 中也会发生相同的过程,但决定是否为此导入语句发出 import 或 require 调用的规则会有所不同,因为 --moduleResolution bundler 需要使用 --module esnext 或 --module preserve。在这两种模式下,ESM import 声明总是作为 ESM import 声明发出,因此 TypeScript 的模块解析器会接收到该信息,并在从 "pkg" 解析 "exports" 时使用 "import" 条件。
🌐 The same process happens in --moduleResolution bundler, but the rules for deciding whether to emit an import or require call for this import statement will be different, since --moduleResolution bundler necessitates using --module esnext or --module preserve. In both of those modes, ESM import declarations always emit as ESM import declarations, so TypeScript’s module resolver will receive that information and use the "import" condition as it resolves "exports" from "pkg".
这个解释可能有些不直观,因为 --moduleResolution bundler 通常与 --noEmit 一起使用——打包工具通常处理原始 .ts 文件,并对未转换的 import 或 require 执行模块解析。然而,为了一致性,TypeScript 仍然使用由 module 决定的假设输出(emit)来指导模块解析和类型检查。这使得 --module preserve 成为当运行时或打包工具处理原始 .ts 文件时的最佳选择,因为它意味着没有任何转换。在 --module preserve --moduleResolution bundler 下,你可以在同一个文件中编写导入(imports)和引入(requires),它们将分别按照 import 和 require 条件进行解析:
🌐 This explanation may be somewhat unintuitive, since --moduleResolution bundler is usually used in combination with --noEmit—bundlers typically process raw .ts files and perform module resolution on untransformed imports or requires. However, for consistency, TypeScript still uses the hypothetical emit decided by module to inform module resolution and type checking. This makes --module preserve the best choice whenever a runtime or bundler is operating on raw .ts files, since it implies no transformation. Under --module preserve --moduleResolution bundler, you can write imports and requires in the same file that will resolve with the import and require conditions, respectively:
ts// index.tsimport pkg1 from "pkg"; // Resolved with "import" conditionimport pkg2 = require("pkg"); // Resolved with "require" condition
隐式和强制选项
🌐 Implied and enforced options
--moduleResolution bundler必须与--module esnext或--module preserve配对。--moduleResolution bundler意味着--allowSyntheticDefaultImports。
支持的功能
🌐 Supported features
paths✅baseUrl✅node_modules包查找 ✅- package.json
"exports"✅ 根据语法匹配types、import/require - package.json
"imports"和自定义名导入 ✅ 匹配types、import/require,具体取决于语法 - package.json
"typesVersions"✅ - 相对于包的路径 ✅ 当
exports不存在时 - 完整的相对路径 ✅
- 无扩展名的相对路径 ✅
- 目录模块 ✅
node10(前称 node)
🌐 node10 (formerly known as node)
--moduleResolution node 在 TypeScript 5.0 中被重命名为 node10(保留 node 作为向后兼容的别名)。它反映了 Node.js v12 之前版本中的 CommonJS 模块解析算法。不应再使用它。
支持的功能
🌐 Supported features
paths✅baseUrl✅node_modules包查找 ✅- package.json
"exports"❌ - package.json
"imports"和自定义名称导入 ❌ - package.json
"typesVersions"✅ - 包相对路径 ✅
- 完整的相对路径 ✅
- 无扩展名的相对路径 ✅
- 目录模块 ✅
classic
不要使用 classic。
🌐 Do not use classic.