模块语法
TypeScript 编译器支持 TypeScript 和 JavaScript 文件中的标准 ECMAScript 模块语法,以及 JavaScript 文件中的许多形式的 CommonJS 语法。
此外,还有一些 TypeScript 特定的语法扩展,可以在 TypeScript 文件和/或 JSDoc 注释中使用。
导入和导出 TypeScript 特定声明
类型别名、接口、枚举和命名空间可以使用 export 修饰符从模块中导出,就像任何标准 JavaScript 声明一样:
ts
它们也可以在命名导出中引用,甚至可以与标准 JavaScript 声明一起引用:
ts
导出的类型(以及其他 TypeScript 特定声明)可以使用标准的 ECMAScript 导入语法导入:
ts
当使用命名空间导入或导出时,如果在类型位置引用,导出的类型将在命名空间上可用:
ts
仅限类型的导入和导出
在将导入和导出转换为 JavaScript 代码时,默认情况下,TypeScript 会自动省略(不生成)仅在类型位置使用的导入语句和仅涉及类型的导出语句。你可以使用仅限类型的导入和导出来强制这样做,并明确指出省略。使用 import type 编写的导入声明,使用 export type { ... } 编写的导出声明,以及带有 type 关键字前缀的导入或导出标识符都将在生成的 JavaScript 代码中被省略。
ts
import type 其实也可以导入值,但由于它们在生成的 JavaScript 代码中不存在,因此只能在非生成位置使用:
ts
仅限类型的导入声明不能同时声明默认导入和命名绑定,因为不清楚 type 是应用于默认导入还是整个导入声明。可以将导入声明拆分为两个声明,或者将 default 用作命名绑定:
ts
import() 类型
TypeScript 提供了一种类似于 JavaScript 动态 import 的类型语法,用于引用模块的类型而无需编写导入声明:
ts
这在 JavaScript 文件的 JSDoc 注释中特别有用,因为否则无法导入类型:
ts
export = 和 import = require()
在输出 CommonJS 模块时,TypeScript 文件可以使用与 JavaScript module.exports = ... 和 const mod = require("...") 直接对应的语法:
ts
相比 JavaScript,选择使用这种语法是因为变量声明和属性赋值不能引用 TypeScript 类型,而特定的 TypeScript 语法可以:
ts
环境模块
TypeScript 支持在脚本文件(非模块文件)中声明在运行时存在但没有对应文件的模块。这些*环境模块(ambient module)*通常表示运行时提供的模块,比如 Node.js 中的 "fs" 或 "path":
ts
一旦环境模块被加载到 TypeScript 程序中,TypeScript 将会识别其他文件中对声明模块的导入:
ts
环境模块声明很容易与模块扩展混淆,因为它们使用相同的语法。如果文件是一个模块,换言之它具有顶级的 import 或 export 语句(或受到 --moduleDetection force 或 auto 的影响),这个模块声明语法将变成模块扩展:
ts
环境模块可以在模块声明体内使用导入语句来引用其他模块,而不会将包含该导入语句的文件转换为模块(这将使环境模块声明成为模块扩展):
ts
*模式(pattern)*环境模块的名称中包含一个 * 通配符,用于匹配导入路径中的零个或多个字符。我们可以借此声明由自定义加载器提供的模块:
ts
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 模块。
- 如果文件夹层级最接近的 package.json 文件中包含 "type": "module",那么.ts/.tsx/.js/.jsx/.d.ts文件将根据此确定为 ES 模块,否则为 CommonJS 模块。
输入的 .ts/.tsx/.mts/.cts 文件的检测到的模块格式决定了生成的 JavaScript 文件的模块格式。例如,一个完全由 .ts 文件组成的项目默认情况下会在 --module nodenext 下全部生成 CommonJS 模块,通过在项目的 package.json 中添加 "type": "module" 可以使其全部生成 ES 模块。
互操作规则
- ES 模块引用 CommonJS 模块:
- CommonJS 模块引用 ES 模块:
- require不能引用 ES 模块。对于 TypeScript,这包括在被检测为 CommonJS 模块的文件中的- import语句,因为这些- import语句将在生成的 JavaScript 中转换为- require调用。
- 可以使用动态的 import()调用来导入 ES 模块。它返回模块的模块命名空间对象的 Promise(从另一个 ES 模块中的import * as ns from "./module.js"中获得的对象)。
 
生成
每个文件的生成格式取决于每个文件的检测到的模块格式。ESM 生成类似于 --module esnext,但对 import x = require("...") 进行了特殊的转换,而在 --module esnext 中是不允许的:
ts
js
CommonJS 的输出与 --module commonjs 类似,但不会转换动态的 import() 调用。这里的输出是启用 esModuleInterop 的。
ts
js
隐含和强制选项
- --module nodenext或- node16隐含并强制使用同名的- moduleResolution。
- --module nodenext隐含使用- --target esnext。
- --module node16隐含使用- --target es2022。
- --module nodenext或- node16隐含使用- --esModuleInterop。
总结
- 对于所有打算在 Node.js v12 或更高版本中运行的应用程序和库,无论它们是否使用 ES 模块,node16和nodenext是唯一正确的module选项。
- node16和- nodenext根据每个文件的检测到的模块格式生成 CommonJS 或 ESM 格式的文件。
- Node.js 在 ESM 和 CJS 之间的互操作规则反映在类型检查中。
- ESM 生成将 import x = require("...")转换为使用createRequire导入构造的require调用。
- CommonJS 生成不对动态的 import()调用进行转换,因此 CommonJS 模块可以异步导入 ES 模块。
es2015, es2020, es2022, esnext
总结
- 对于打包工具、Bun 和 tsx,请使用 --moduleResolution bundler和esnext。
- 不适用于 Node.js。在 package.json 中使用 "type": "module"并配合node16或nodenext选项以生成适用于 Node.js 的 ES 模块。
- 非声明文件中不允许使用 import mod = require("mod")。
- es2020添加了对- import.meta属性的支持。
- es2022添加了对顶层- await的支持。
- esnext是一个不断发展的目标,可能包括对 ECMAScript 模块的 Stage 3 提案的支持。
- 生成的文件是 ES 模块,但依赖项可以是任何格式。
示例
ts
js
commonjs
总结
- 你可能不应该使用这个选项。请使用 node16或nodenext以生成适用于 Node.js 的 CommonJS 模块。
- 生成的文件是 CommonJS 模块,但依赖项可以是任何格式。
- 动态的 import()会转换为require()调用的 Promise。
- esModuleInterop会影响默认导入和命名空间导入的输出代码。
示例
输出代码是在
esModuleInterop: false的情况下显示的。
ts
js
ts
js
system
概述
- 专为与 SystemJS 模块加载器配合使用而设计。
示例
ts
js
amd
概述
- 专为像 RequireJS 这样的 AMD 加载器设计。
- 你大概不应该使用它,而应该使用打包工具。
- 生成的文件是 AMD 模块,但依赖可以是任意格式。
- 支持 outFile选项。
示例
ts
js
umd
概述
- 专为 AMD 或 CommonJS 加载器设计。
- 不像大多数其他 UMD 包装器一样暴露全局变量。
- 你大概不应该使用它,而应该使用打包工具。
- 生成的文件是 UMD 模块,但依赖可以是任意格式。
示例
ts
js
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
有关 TypeScript 模块解析方式的解释,请参阅TypeScript 模仿主机的模块解析,但带有类型。
相对文件路径解析
TypeScript 的 moduleResolution 算法都支持使用包含文件扩展名的相对路径引用模块(根据上述规则进行替换):
ts
无扩展名的相对路径
在某些情况下,运行时或捆绑器允许省略相对路径中的 .js 文件扩展名。TypeScript 在 moduleResolution 设置和上下文对运行时或捆绑器的指示支持此行为的情况下,也支持此行为:
ts
如果 TypeScript 确定运行时将根据模块说明符 "./a" 查找 ./a.js,则 ./a.js 将经过扩展名替换,在此示例中解析为文件 a.ts。
Node.js 的 import 路径不支持无扩展名的相对路径,而且在 package.json 文件中指定的文件路径中也不总是支持。尽管某些运行时和捆绑器支持此功能,但 TypeScript 目前不支持省略 .mjs/.mts 或 .cjs/.cts 文件扩展名。
目录模块(索引文件解析)
在某些情况下,可以将目录(而不是文件)作为模块的引用。在最简单和最常见的情况下,运行时或捆绑器会在目录中查找名为 index.js 的文件。TypeScript 支持此行为,前提是 moduleResolution 设置和上下文对运行时或捆绑器的指示支持它:
ts
如果 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
ts
对于使用打包器构建的应用程序,常见做法是在打包器配置中定义便捷路径别名,然后使用 paths 通知 TypeScript 这些别名:
json
paths 对生成不起作用
paths 选项不会改变 TypeScript 生成的代码中的导入路径。因此,虽然我们可以在 TypeScript 中创建看似有效的路径别名,但在运行时会导致崩溃:
json
ts
尽管为打包的应用程序设置 paths 是可行的,但是发布的库千万不要这样做,因为生成的 JavaScript 在没有为 TypeScript 和打包器设置相同别名的情况下无法正常工作。对于库和应用程序,可以考虑使用 package.json 的 "imports" 作为方便的 paths 别名的标准替代方案。
paths 不应指向 monorepo 包或 node_modules 包
虽然与 paths 别名匹配的模块标识符是裸标识符,但一旦解析了别名,模块解析将继续把解析后的路径作为相对路径进行。因此,在 paths 别名匹配时不会发生 node_modules 包查找中发生的解析特性,包括 package.json 的 "exports" 字段支持。如果使用 paths 指向 node_modules 包,可能会导致意外的行为:
ts
尽管此配置可能模拟某些包解析行为,但它会覆盖包的 package.json 文件定义的任何 main、types、exports 和 typesVersions,并且从该包中导入的内容可能在运行时失败。
相同的注意事项也适用于在 monorepo 中相互引用的包。不要使用 paths 来使 TypeScript 人为地解析 "@my-scope/lib" 为一个同级包,而是最好使用通过 npm、yarn 或 pnpm 将你的包符号链接到 node_modules,这样 TypeScript 和运行时或打包器将执行真正的 node_modules 包查找。如果 monorepo 包将被发布到 npm,则尤其重要——一旦用户安装了这些包,它们将通过 node_modules 包查找相互引用,并且使用工作区从而你借此可以在本地开发期间测试该行为。
与 baseUrl 的关系
如果提供了 baseUrl,则每个 paths 数组中的值将相对于 baseUrl 进行解析。否则,它们将相对于定义它们的 tsconfig.json 文件进行解析。
通配符替换
paths 模式可以包含单个 * 通配符,用于匹配任意字符串。然后,可以在文件路径值中使用 * 标记来替换匹配的字符串:
json
当解析 "@app/components/Button" 的导入时,TypeScript 将匹配 @app/*,将 * 绑定到 components/Button,然后尝试相对于 tsconfig.json 路径解析路径 ./src/components/Button。此查找的其余部分将根据 moduleResolution 设置遵循与任何其他相对路径查找相同的规则。
当多个模式匹配同一个模块标识符时,将使用具有最长匹配前缀(在任何 * 标记之前)的模式:
json
当解析 "foo/bar" 的导入时,所有三个 paths 模式都匹配,但是最后一个被使用,因为 "foo/bar" 比 "foo/" 和 "" 长。
回退
对于每个路径映射,你可以提供多个文件路径。如果一个路径解析失败,则会尝试数组中的下一个路径,直到解析成功或达到数组的末尾。
json
baseUrl
baseUrl是为 AMD 模块加载器设计的。如果你不使用 AMD 模块加载器,可能也不需要使用baseUrl。自 TypeScript 4.1 起,paths不再需要baseUrl,并且不应仅用于设置paths值解析的目录。
baseUrl 编译选项可以与任何 moduleResolution 模式结合使用,并且可以指定裸模块标识符(bare specifier)(不以 ./、../ 或 / 开头的模块标识符)的解析目录。在支持它们的 moduleResolution 模式中,baseUrl 优先级高于 node_modules软件包查找。
在执行 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_modules 包查找触发的包目录解析时,遵循 Node.js package.json 的 "exports" 规范。
TypeScript 在将模块标识符通过 "exports" 解析为文件路径时完全按照 Node.js 的实现。但是,一旦解析出文件路径,TypeScript 仍然会尝试多个文件扩展名,以便优先找到类型。
在通过有条件的 "exports" 进行解析时,TypeScript 总是匹配 "types" 和 "default" 条件(如果存在)。此外,TypeScript 还将匹配形如 "types@{selector}" 的带有版本的类型条件(其中 {selector} 是符合 "typesVersions" 版本选择器的字符串),遵循与 "typesVersions" 实现的相同版本匹配规则。其他不可配置的条件取决于 moduleResolution 模式,并在以下章节中指定。可以使用 customConditions 编译器选项配置其他条件进行匹配。
请注意,"exports" 的存在会阻止未在 "exports" 中明确列出或与模式匹配的子路径被解析。
示例:子路径、条件和扩展名替换
场景:在具有以下 package.json 的包目录中,使用条件 ["types", "node", "require"](由 moduleResolution 的设置和触发模块解析请求的上下文确定)请求了 "pkg/subpath":
json
包目录内的解析过程:
- 是否存在 "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
包目录内的解析过程:
- 是否存在 "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,在具有以下 package.json 的包目录中,使用条件 ["types", "node", "import"](由 moduleResolution 的设置和触发模块解析请求的上下文确定)请求了 "pkg/subpath":
json
包目录内的解析过程:
- 是否存在 "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
包目录内的解析过程:
- 是否存在 "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" 阻止其他子路径
场景:在包目录中请求 "pkg/dist/index.js",该目录下的 package.json 如下所示:
json
包目录内的解析过程:
- 是否存在 "exports"? 是。
- exports的值是一个字符串,它必须是相对于包根目录 (- ".") 的文件路径。
- 请求的路径 "pkg/dist/index.js"是否是包根目录? 不是,它包含子路径dist/index.js。
- 解析失败;返回 undefined。
如果没有 "exports",该请求可能会成功,但是 "exports" 的存在会阻止解析无法通过 "exports" 匹配的子路径。
package.json 的 "typesVersions"
node_modules 包或目录模块可以在其 package.json 中指定 "typesVersions" 字段,来根据 TypeScript 编译器版本和正在解析的子路径,重定向 TypeScript 的解析过程。包作者借此可以在一组类型定义中包含新的 TypeScript 语法,同时为了与旧的 TypeScript 版本向后兼容提供另一组类型定义(通过类似 downlevel-dts 的工具)。"typesVersions" 在所有的 moduleResolution 模式下都受支持;但是,在读取 package.json 的 "exports" 时不会读取该字段。
示例:重定向所有请求到子目录
场景:某模块使用 TypeScript 5.2 导入 "pkg",其中 node_modules/pkg/package.json 文件内容如下:
json
解析过程:
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
解析过程:
1.(根据编译器选项)存在 "exports" 吗?否。
2. 存在 "typesVersions" 吗?是。
3. TypeScript 版本是否 <4.0?是。记住映射关系 "index.d.ts": ["index.v3.d.ts"]。
4. 我们是否在解析包名后的子路径?不是,只是根路径下的 "pkg"。
5. 存在 "types" 吗?是的。
6. "typesVersions" 中是否有任何键与 ./index.d.ts 匹配?是,"index.d.ts" 匹配。
7. 路径 ./index.v3.d.ts 是否具有 TypeScript 文件扩展名?是的,因此不使用扩展名替换。
8. 如果文件存在,则返回路径 ./index.v3.d.ts,否则返回 undefined。
package.json 的 "main" 和 "types"
如果一个目录的 package.json "exports" 字段没有被读取(要么是由于编译器选项,要么是因为该字段不存在,要么是因为该目录被解析为目录模块,而不是node_modules 包),并且模块标识符在包名或包含 package.json 的目录后没有子路径时,TypeScript 将按照以下顺序尝试从这些 package.json 字段中解析,以查找包或目录的主模块:
- "types"
- "typings"(已弃用)
- "main"
在 "types" 字段找到的声明文件被假定为与在 "main" 中找到的实现文件相对应。如果找不到或无法解析 "types" 和 "typings",TypeScript 将读取 "main" 字段并执行扩展名替换以找到声明文件。
当将一个带有类型的包发布到 npm 上时,建议即使扩展名替换或 package.json "exports" 使其变得不必要,也要包含一个 "types" 字段,因为 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
package.json "imports" 和自身名称导入
如果 moduleResolution 设置为 node16、nodenext 或 bundler,且未禁用 resolvePackageJsonImports,TypeScript 将尝试通过导入文件的最近祖先 package.json 的 "imports" 字段解析以 # 开头的导入路径。类似地,当启用 package.json "exports" 查找时,TypeScript 将尝试通过当前包名称开始的导入路径(即导入文件最近祖先 package.json 中的 "name" 字段的值)通过该 package.json 的 "exports" 字段进行解析。这两个特性允许一个包中的文件导入同一包中的其他文件,替换相对导入路径。
TypeScript 在解析 "imports" 和自引用时,完全遵循 Node.js 的解析算法,直到解析出文件路径为止。在那一点上,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。
这个变化使得包作者能够编写 "imports" 和 "exports" 字段,只引用将要发布到 npm 的编译输出,同时仍然允许本地开发使用原始的 TypeScript 源文件。
示例:具有条件的本地项目
场景:在一个带有 tsconfig.json 和 package.json 的项目目录中,"/src/main.mts" 在条件 ["types", "node", "import"](由 moduleResolution 的设置和触发模块解析请求的上下文决定)下引入了 "#utils"。
json
json
解析过程:
- 导入路径以 #开头,尝试通过"imports"进行解析。
- 最近的上层 package.json中存在"imports"吗?是。
- "imports"对象中是否存在- "#utils"?是。
- imports["#utils"]的值是一个对象,它必须指定条件。
- 第一个条件 "import"是否与此请求匹配?是。
- 我们是否应该尝试将输出路径映射到输入路径?是,因为:
- package.json是否在- node_modules中?不是,它在本地项目中。
- tsconfig.json是否在- package.json目录中?是的。
 
- 在 ./dist/utils.d.mts中,用rootDir替换outDir前缀。./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"](由 moduleResolution 的设置和触发模块解析请求的上下文决定)导入了 "#internal/utils",具有以下 package.json:
json
解析过程:
- 导入路径以 #开头,尝试通过"imports"进行解析。
- 最近的上层 package.json中存在"imports"吗?是。
- "imports"对象中是否存在- "#internal/utils"?否,检查模式匹配。
- 是否有任何带有 *的键匹配"#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"(#packagejson-exports)或 "imports"(#package.json-imports-和自身名称导入)使用 "import" 或 "require" 条件)。
被确定为 CommonJS 格式的 TypeScript 文件在默认情况下仍然可以使用
import和export语法,但生成的 JavaScript 代码将使用require和module.exports。这意味着经常会看到使用require算法解析的import语句。如果这造成了困惑,可以启用verbatimModuleSyntax编译选项,该选项禁止使用会被生成为require调用的import语句。
需要注意的是,动态的 import() 调用始终使用 import 算法进行解析,符合 Node.js 的行为。然而,import() 类型的解析是根据导入文件(译注:不是被导入,例如如果 A 导入 B,则这里说的是 A)的格式进行的(为了与现有的 CommonJS 格式的类型声明向后兼容):
ts
隐含和强制选项
- --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"。
这与在 CommonJS 模式下 node16 和 nodenext 解析的行为非常相似,但在 bundler 中,用于解析 package.json 中 "exports" 和 "imports" 的条件始终是 "types" 和 "import"。为了理解其中的原因,让我们来看一下在 nodenext 中一个 .ts 文件中的导入会发生什么:
ts
在 --module nodenext --moduleResolution nodenext 中,--module 设置首先确定导入将作为 import 还是 require 调用写入 .js 文件,并将该信息传递给 TypeScript 的模块解析器,该解析器根据需要匹配 "import" 或 "require" 条件。这确保了 TypeScript 的模块解析过程,尽管是从输入的 .ts 文件开始,但能够反映在运行输出的 .js 文件时 Node.js 的模块解析过程将会发生什么。
另一方面,在使用打包工具时,通常打包工具直接处理原始的 .ts 文件,并基于未经转换的 import 语句运行其模块解析过程。在这种情况下,考虑 TypeScript 如何生成 import 并没有太多意义,因为 TypeScript 根本不用于生成任何内容。对于打包工具来说,import 就是 import,require 就是 require,因此用于解析 package.json 中的 "exports" 和 "imports" 的条件由输入的 .ts 文件中的语法决定。同样,TypeScript 在 --moduleResolution bundler 中使用的模块解析过程中的条件也由输入 TypeScript 文件中的语法决定,只是 require 调用目前根本不会被解析:
ts
由于 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 选项。