模块语法
TypeScript 编译器识别 TypeScript 和 JavaScript 文件中的标准 ECMAScript 模块语法,以及 JavaScript 文件中许多形式的 CommonJS 语法。
还有一些特定于 TypeScript 的语法扩展,可以在 TypeScript 文件和/或 JSDoc 注释中使用。
导入和导出特定于 TypeScript 的声明
类型别名、接口、枚举和命名空间可以使用 export
修饰符从模块导出,就像任何标准的 JavaScript 声明一样
ts
// Standard JavaScript syntax...export function f() {}// ...extended to type declarationsexport type SomeType = /* ... */;export interface SomeInterface { /* ... */ }
它们也可以在命名导出中引用,即使与标准 JavaScript 声明的引用并存
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.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 中,因此它们只能在非输出位置使用。
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.tsimport fs = require("fs");export = fs.readFileSync("...");// @Filename: main.js"use strict";const fs = require("fs");module.exports = fs.readFileSync("...");
这种语法之所以被使用,是因为变量声明和属性赋值不能引用 TypeScript 类型,而特殊的 TypeScript 语法可以
ts
// @Filename: a.tsinterface Options { /* ... */ }module.exports = Options; // Error: 'Options' only refers to a type, but is being used as a value here.export = Options; // Ok// @Filename: b.tsconst Options = require("./a");const options: Options = { /* ... */ }; // Error: 'Options' refers to a value, but is being used as a type here.// @Filename: c.tsimport Options = require("./a");const options: Options = { /* ... */ }; // Ok
环境模块
TypeScript 在脚本(非模块)文件中支持一种语法,用于声明在运行时存在的但没有对应文件的模块。这些环境模块通常代表运行时提供的模块,例如 Node.js 中的 "fs"
或 "path"
ts
declare module "path" {export function normalize(p: string): string;export function join(...paths: any[]): string;export var sep: string;}
一旦环境模块被加载到 TypeScript 程序中,TypeScript 将识别其他文件中对声明模块的导入
ts
// 👇 Ensure the ambient module is loaded -// may be unnecessary if path.d.ts is included// by the project tsconfig.json somehow./// <reference path="path.d.ts" />import { normalize, join } from "path";
环境模块声明很容易与 模块增强 混淆,因为它们使用相同的语法。当文件是模块时,此模块声明语法变为模块增强,这意味着它具有顶级 import
或 export
语句(或受 --moduleDetection force
或 auto
影响)
ts
// Not an ambient module declaration anymore!export {};declare module "path" {export function normalize(p: string): string;export function join(...paths: any[]): string;export var sep: string;}
环境模块可以在模块声明体中使用导入来引用其他模块,而不会将包含文件转换为模块(这将使环境模块声明成为模块增强)
ts
declare module "m" {// Moving this outside "m" would totally change the meaning of the file!import { SomeType } from "other";export function f(): SomeType;}
模式 环境模块在其名称中包含单个 *
通配符,匹配导入路径中的零个或多个字符。这对于声明由自定义加载器提供的模块很有用
ts
declare module "*.html" {const content: string;export default content;}
module
编译器选项
本节讨论每个 module
编译器选项值的详细信息。有关该选项的更多背景信息以及它如何融入整个编译过程,请参阅 模块输出格式 理论部分。简而言之,module
编译器选项在历史上仅用于控制发出 JavaScript 文件的输出模块格式。但是,最近的 node16
和 nodenext
值描述了 Node.js 模块系统的广泛特征,包括支持哪些模块格式、如何确定每个文件的模块格式以及不同模块格式如何交互。
node16
, nodenext
Node.js 支持 CommonJS 和 ECMAScript 模块,并有特定规则来规定每个文件可以采用哪种格式以及两种格式如何交互。node16
和 nodenext
描述了 Node.js 双格式模块系统的全部行为,并以 CommonJS 或 ESM 格式发出文件。这与其他所有 module
选项不同,这些选项与运行时无关,并强制所有输出文件采用单一格式,由用户确保输出对他们的运行时有效。
一个常见的误解是,
node16
和nodenext
仅发出 ES 模块。实际上,node16
和nodenext
描述的是支持 ES 模块的 Node.js 版本,而不仅仅是使用 ES 模块的项目。根据每个文件的检测到的模块格式,ESM 和 CommonJS 发射都受支持。由于node16
和nodenext
是唯一反映 Node.js 双模块系统复杂性的module
选项,因此它们是所有旨在在 Node.js v12 或更高版本中运行的应用程序和库的唯一正确的module
选项,无论它们是否使用 ES 模块。
node16
和 nodenext
目前是相同的,区别在于它们暗示不同的 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 模块引用 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"; // transformedconst 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")); // transformedconst dynamic = import("mod"); // not transformed
隐含和强制选项
--module nodenext
或node16
意味着并强制使用同名的moduleResolution
。--module nodenext
意味着--target esnext
。--module node16
意味着--target es2022
。--module nodenext
或node16
意味着--esModuleInterop
。
总结
node16
和nodenext
是所有旨在在 Node.js v12 或更高版本中运行的应用程序和库的唯一正确的module
选项,无论它们是否使用 ES 模块。node16
和nodenext
会根据每个文件的 检测到的模块格式 以 CommonJS 或 ESM 格式发出文件。- Node.js 中 ESM 和 CJS 之间的互操作性规则反映在类型检查中。
- ESM 发出将
import x = require("...")
转换为从createRequire
导入构建的require
调用的转换。 - CommonJS 发出将动态
import()
调用保持不变,因此 CommonJS 模块可以异步导入 ES 模块。
es2015
、es2020
、es2022
、esnext
总结
- 对于打包工具、Bun 和 tsx,请使用
esnext
以及--moduleResolution bundler
。 - 不要用于 Node.js。对于 Node.js,请使用
node16
或nodenext
,并在 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
总结
- 您可能不应该使用它。请使用
node16
或nodenext
来为 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
概述
- 专为与 SystemJS 模块加载器 一起使用而设计。
示例
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.tsexport {};// @Filename: b.tsimport {} from "./a.js"; // ✅ Works in every `moduleResolution`
无扩展名相对路径
在某些情况下,运行时或打包器允许从相对路径中省略 `.js` 文件扩展名。TypeScript 支持这种行为,其中 `moduleResolution` 设置和上下文表明运行时或打包器支持它。
ts
// @Filename: a.tsexport {};// @Filename: b.tsimport {} from "./a";
如果 TypeScript 确定运行时将针对模块标识符 `"./a"` 执行对 `./a.js` 的查找,那么 `./a.js` 将经过扩展名替换,并解析为本例中的文件 `a.ts`。
无扩展名相对路径在 Node.js 中的 `import` 路径中不受支持,并且在 `package.json` 文件中指定的路径中也不总是受支持。TypeScript 目前从不支持省略 `.mjs` / `.mts` 或 `.cjs` / `.cts` 文件扩展名,即使某些运行时和打包器支持。
目录模块(索引文件解析)
在某些情况下,目录而不是文件可以被引用为模块。在最简单和最常见的情况下,这涉及运行时或捆绑器在目录中查找一个index.js
文件。TypeScript 支持这种行为,其中moduleResolution
设置和上下文表明运行时或捆绑器支持它。
ts
// @Filename: dir/index.tsexport {};// @Filename: b.tsimport {} from "./dir";
如果 TypeScript 确定运行时将根据模块标识符"./dir"
查找./dir/index.js
,那么./dir/index.js
将经历扩展名替换,在本例中解析为文件dir/index.ts
。
目录模块也可以包含一个 package.json 文件,其中支持对"main"
和 "types"
字段的解析,并优先于index.js
查找。在目录模块中也支持"typesVersions"
字段。
请注意,目录模块与node_modules
包不同,只支持包可用功能的一部分,并且在某些情况下根本不支持。Node.js 将它们视为遗留功能。
paths
概述
TypeScript 提供了一种方法,可以使用paths
编译器选项覆盖编译器对裸标识符的模块解析。虽然该功能最初是为 AMD 模块加载器(一种在 ESM 存在或捆绑器广泛使用之前在浏览器中运行模块的方式)而设计的,但它在当今仍然有用,当运行时或捆绑器支持 TypeScript 未建模的模块解析功能时。例如,当使用--experimental-network-imports
运行 Node.js 时,您可以手动为特定https://
导入指定本地类型定义文件。
json
{"compilerOptions": {"module": "nodenext","paths": {}}}
ts
// Typed by ./node_modules/@types/lodash/index.d.ts due to `paths` entry
使用打包工具构建的应用程序通常会在其打包工具配置中定义方便的路径别名,然后使用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
文件中定义的任何main
、types
、exports
和typesVersions
,并且来自该包的导入可能会在运行时失败。
对于在单仓库中相互引用的包,也存在同样的注意事项。与其使用 `paths` 让 TypeScript 人工地将 `"@my-scope/lib"` 解析到兄弟包,不如通过 npm、yarn 或 pnpm 使用工作区将你的包符号链接到 `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
模式结合使用,并指定一个目录,从该目录解析裸说明符(不以./
、../
或/
开头的模块说明符)。baseUrl
比node_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
包查找都具有以下结构(从优先级更高的裸标识符规则开始,例如 paths
、baseUrl
、自命名导入和 package.json "imports"
查找已用尽后)
- 对于导入文件的每个祖先目录,如果其中存在
node_modules
目录- 如果
node_modules
中存在与包同名的目录- 尝试从包目录解析类型。
- 如果找到结果,则返回结果并停止搜索。
- 如果
node_modules/@types
中存在与包同名的目录- 尝试从
@types
包目录解析类型。 - 如果找到结果,则返回结果并停止搜索。
- 尝试从
- 如果
- 重复对所有
node_modules
目录的先前搜索,但这次允许 JavaScript 文件作为结果,并且不要在@types
目录中搜索。
所有 moduleResolution
模式(除了 classic
)都遵循这种模式,而它们从包目录(一旦找到)解析的方式的细节有所不同,将在以下部分中解释。
package.json "exports"
当 moduleResolution
设置为 node16
、nodenext
或 bundler
,并且 resolvePackageJsonExports
未禁用时,TypeScript 会遵循 Node.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"}}}
包目录内的解析过程
"exports"
是否存在?是。"exports"
是否具有"./subpath"
条目?是。exports["./subpath"]
处的值为一个对象——它必须指定条件。- 第一个条件
"import"
是否与该请求匹配?否。 - 第二个条件
"require"
是否与该请求匹配?是。 - 路径
"./subpath/index.cjs"
是否具有可识别的 TypeScript 文件扩展名?否,因此使用扩展名替换。 - 通过 扩展名替换,尝试以下路径,返回第一个存在的路径,否则返回
undefined
./subpath/index.cts
./subpath/index.d.cts
./subpath/index.cjs
如果 ./subpath/index.cts
或 ./subpath.d.cts
存在,则解析完成。否则,解析将根据 node_modules
包查找 规则,在 node_modules/@types/pkg
和其他 node_modules
目录中搜索类型。如果未找到类型,则对所有 node_modules
进行第二次遍历,解析为 ./subpath/index.cjs
(假设它存在),这算作成功解析,但不会提供类型,导致 any
类型的导入,如果启用 noImplicitAny
,则会导致错误。
示例:显式 "types"
条件
场景:在具有以下 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"}}}}
包目录内的解析过程
"exports"
是否存在?是。"exports"
是否具有"./subpath"
条目?是。exports["./subpath"]
处的值为一个对象——它必须指定条件。- 第一个条件
"import"
是否与该请求匹配?是。 exports["./subpath"].import
处的值为对象,它必须指定条件。- 第一个条件
"types"
是否与该请求匹配?是。 - 路径
"./types/subpath/index.d.mts"
是否具有识别的 TypeScript 文件扩展名?是,因此不要使用扩展名替换。 - 如果文件存在,则返回路径
"./types/subpath/index.d.mts"
,否则返回undefined
。
示例:版本化的 "types"
条件
场景:使用 TypeScript 4.7.5,使用条件 ["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"}}}
包目录内的解析过程
"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
。
示例:子路径模式
场景:在具有以下 package.json 的包目录中,使用条件 ["types", "node", "import"]
(由 moduleResolution
设置和触发模块解析请求的上下文决定)请求 "pkg/wildcard.js"
json
{"name": "pkg","type": "module","exports": {"./*.js": {"types": "./types/*.d.ts","default": "./dist/*.js"}}}
包目录内的解析过程
"exports"
是否存在?是。"exports"
是否具有"./wildcard.js"
条目?否。- 是否有任何包含
*
的键匹配"./wildcard.js"
?是的,"./*.js"
匹配并将wildcard
设置为替换。 exports["./*.js"]
处的值为一个对象——它必须指定条件。- 第一个条件
"types"
是否与该请求匹配?是。 - 在
./types/*.d.ts
中,用替换wildcard
替换*
。./types/wildcard.d.ts
- 路径
"./types/wildcard.d.ts"
是否具有识别的 TypeScript 文件扩展名?是的,所以不要使用扩展名替换。 - 如果文件存在,则返回路径
"./types/wildcard.d.ts"
,否则返回undefined
。
示例:"exports"
阻止其他子路径
场景:在具有以下 package.json 的包目录中请求 "pkg/dist/index.js"
json
{"name": "pkg","main": "./dist/index.js","exports": "./dist/index.js"}
包目录内的解析过程
"exports"
是否存在?是。exports
中的值是一个字符串——它必须是包根目录的路径("."
)。- 请求
"pkg/dist/index.js"
是针对包根目录的吗?不是,它包含一个子路径dist/index.js
。 - 解析失败;返回
undefined
。
如果没有 "exports"
,请求可能会成功,但 "exports"
的存在会阻止解析任何无法通过 "exports"
匹配的子路径。
package.json "typesVersions"
一个 node_modules
包 或 目录模块 可以在其 package.json 中指定一个 "typesVersions"
字段,以根据 TypeScript 编译器版本重定向 TypeScript 的解析过程,对于 node_modules
包,则根据要解析的子路径重定向。这允许包作者在一个类型定义集中包含新的 TypeScript 语法,同时提供另一个类型定义集以向后兼容旧版本的 TypeScript(通过像 downlevel-dts 这样的工具)。"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/*"]}}}
解析过程
- (取决于编译器选项)
"exports"
是否存在?否。 "typesVersions"
是否存在?**是。**- TypeScript 版本是否
>=3.1
?**是。请记住映射"*": ["ts3.1/*"]
。** - 我们是否在包名之后解析子路径?**否,只是根目录
"pkg"
。** "types"
是否存在?**是。**"typesVersions"
中的任何键是否与./index.d.ts
匹配?**是,"*"
匹配并将index.d.ts
设置为替换。**- 在
ts3.1/*
中,用替换./index.d.ts
替换*
:**ts3.1/index.d.ts
**。 - 路径
./ts3.1/index.d.ts
是否具有识别的 TypeScript 文件扩展名?**是,因此不要使用扩展名替换。** - 如果文件存在,则返回路径
./ts3.1/index.d.ts
,否则返回undefined
。
示例:重定向对特定文件的请求
场景:模块使用 TypeScript 3.9 导入"pkg"
,其中node_modules/pkg/package.json
为
json
{"name": "pkg","version": "1.0.0","types": "./index.d.ts","typesVersions": {"<4.0": { "index.d.ts": ["index.v3.d.ts"] }}}
解析过程
- (取决于编译器选项)
"exports"
是否存在?否。 "typesVersions"
是否存在?**是。**- TypeScript 版本是否
<4.0
?**是。请记住映射"index.d.ts": ["index.v3.d.ts"]
。** - 我们是否在包名之后解析子路径?**否,只是根目录
"pkg"
。** "types"
是否存在?**是。**"typesVersions"
中的任何键是否与./index.d.ts
匹配?**是,"index.d.ts"
匹配。**- 路径
./index.v3.d.ts
是否具有识别的 TypeScript 文件扩展名?**是,因此不要使用扩展名替换。** - 如果文件存在,则返回路径
./index.v3.d.ts
,否则返回undefined
。
和 "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.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"
和自命名导入
当 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 源文件。
示例:带有条件的本地项目
场景:"/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"}}}
解析过程
- 导入路径以
#
开头,尝试通过"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
依赖项
场景:"/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"}}}
解析过程
- 导入路径以
#
开头,尝试通过"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 文件扩展名?否,尝试扩展名替换。 - 通过 扩展名替换,尝试以下路径,返回第一个存在的路径,否则返回
undefined
./dist/internal/utils.mts
./dist/internal/utils.d.mts
./dist/internal/utils.mjs
node16
, nodenext
这些模式反映了 Node.js v12 及更高版本中的模块解析行为。(node16
和 nodenext
目前相同,但如果 Node.js 对其模块系统进行了重大更改,node16
将被冻结,而 nodenext
将被更新以反映新的行为。)在 Node.js 中,ECMAScript 导入的解析算法与 CommonJS require
调用的算法有很大不同。对于每个要解析的模块标识符,首先使用导入文件的语法和模块格式来确定模块标识符将在生成的 JavaScript 中是 import
还是 require
。然后将该信息传递给模块解析器,以确定使用哪种解析算法(以及是否对 package.json "exports"
或 "imports"
使用 "import"
或 "require"
条件)。
默认情况下,被确定为 CommonJS 格式 的 TypeScript 文件仍然可以使用
import
和export
语法,但生成的 JavaScript 将使用require
和module.exports
。这意味着通常会看到使用require
算法解析的import
语句。如果这会导致混淆,可以启用verbatimModuleSyntax
编译器选项,该选项禁止使用将被发出为require
调用的import
语句。
请注意,动态import()
调用始终使用import
算法解析,符合 Node.js 的行为。但是,import()
类型根据导入文件的格式解析(为了与现有的 CommonJS 格式类型声明向后兼容)。
ts
// @Filename: module.mtsimport x from "./mod.js"; // `import` algorithm due to file format (emitted as-written)import("./mod.js"); // `import` algorithm due to syntax (emitted as-written)type Mod = typeof import("./mod.js"); // `import` algorithm due to file formatimport mod = require("./mod"); // `require` algorithm due to syntax (emitted as `require`)// @Filename: commonjs.ctsimport x from "./mod"; // `require` algorithm due to file format (emitted as `require`)import("./mod.js"); // `import` algorithm due to syntax (emitted as-written)type Mod = typeof import("./mod"); // `require` algorithm due to file formatimport mod = require("./mod"); // `require` algorithm due to syntax (emitted as `require`)
隐含和强制选项
--moduleResolution node16
和nodenext
必须与其相应的module
值 配合使用。
支持的功能
功能按优先级顺序排列。
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"
。
这与 node16
和 nodenext
在 CommonJS 模式下解析的行为非常相似,但在 bundler
中,用于解析 package.json "exports"
和 "imports"
的条件始终为 "types"
和 "import"
。为了理解原因,让我们比较一下在 nodenext
中 .ts
文件中的导入会发生什么。
ts
// index.tsimport { foo } from "pkg";
在 --module nodenext --moduleResolution nodenext
中,--module
设置首先 确定 导入将以 import
或 require
调用形式发出到 .js
文件中,并将该信息传递给 TypeScript 的模块解析器,该解析器决定是否相应地匹配 "import"
或 "require"
条件。这确保了 TypeScript 的模块解析过程,尽管是从输入 .ts
文件开始,但反映了 Node.js 在运行输出 .js
文件时模块解析过程将发生的情况。
另一方面,当使用打包器时,打包器通常直接处理原始 .ts
文件,并在未转换的 import
语句上运行其模块解析过程。在这种情况下,考虑 TypeScript 如何发出 import
没有多大意义,因为 TypeScript 根本没有用于发出任何内容。就打包器而言,import
就是 import
,require
就是 require
,因此用于解析 package.json "exports"
和 "imports"
的条件由输入 .ts
文件中看到的语法决定。同样,TypeScript 的模块解析过程在 --moduleResolution bundler
中使用的条件也由输入 TypeScript 文件中的输入语法决定——只是 require
调用目前根本没有解析。
ts
// Some library file:declare function require(module: string): any;// index.tsimport { foo } from "pkg"; // Resolved with "import" conditionimport pkg2 = require("pkg"); // Not allowedconst pkg = require("pkg"); // Not an error, but not resolved to anything// ^? any
由于 TypeScript 目前不支持在 --moduleResolution bundler
中解析 require
调用,因此它确实解析的所有内容都使用 "import"
条件。
隐含和强制选项
--moduleResolution bundler
必须与--module esnext
选项配对。--moduleResolution bundler
意味着--allowSyntheticDefaultImports
。
支持的功能
paths
✅baseUrl
✅node_modules
包查找 ✅- package.json
"exports"
✅ 与types
、import
匹配 - package.json
"imports"
和自命名导入 ✅ 与types
、import
匹配 - package.json
"typesVersions"
✅ - 包相对路径 ✅ 当
exports
不存在时 - 完整相对路径 ✅
- 无扩展名相对路径 ✅
- 目录模块 ✅
node10
(以前称为 node
)
--moduleResolution node
在 TypeScript 5.0 中被重命名为 node10
(保留 node
作为向后兼容的别名)。它反映了 Node.js 版本早于 v12 中存在的 CommonJS 模块解析算法。它不应该再被使用。
支持的功能
paths
✅baseUrl
✅node_modules
包查找 ✅- package.json
"exports"
❌ - package.json
"imports"
和自命名导入 ❌ - package.json
"typesVersions"
✅ - 包相对路径 ✅
- 完整相对路径 ✅
- 无扩展名相对路径 ✅
- 目录模块 ✅
classic
不要使用 classic
。