前置知识: TypeScript

模块解析策略

73 minIntermediate2026/7/20

TypeScript 模块解析策略的形式语义、Node.js 包解析算法、exports 字段、路径映射与生产级配置

引言:从 require 到 import 的解析演化

模块解析(Module Resolution)是编程语言与运行时的核心基础设施之一:当代码中出现 import { foo } from './utils'require('./utils') 时,编译器与运行时需要把字符串 './utils' 映射到磁盘上的具体文件。这个看似简单的字符串到文件的映射过程,在 JavaScript 生态中经历了从 CommonJS 到 ES Modules、从单文件到 monorepo、从 Node.js 到打包工具的复杂演化。

JavaScript 模块解析的复杂性源于三方面历史包袱:

  1. 双模块系统并存:CommonJS(2009)与 ES Modules(2015)在解析语义上不兼容——CJS 允许省略扩展名、动态 require、目录入口;ESM 严格要求扩展名、静态 import、显式入口。
  2. 多运行时多工具链:Node.js、Deno、Bun、浏览器、Vite、webpack、esbuild、Rollup、swc 各自有不同的解析行为,TypeScript 必须为每个目标提供匹配的解析策略。
  3. 包入口的演化:从单一 main 字段到 module/browser 字段,再到 exports 字段的条件导出与子路径导出,包作者可以精细控制不同消费方看到的入口。

TypeScript 的 moduleResolution 选项是这套复杂性的集大成者。本模块的目标是系统梳理 TypeScript 五种解析策略的形式语义、Node.js 包解析算法、package.json 的 exports/imports 字段、路径映射工具与生产级配置,提供 MIT/Stanford/CMU 教学水准的形式化基础与工程实践指南。

1. 历史动机与时间线

1.1 模块解析的演进历程

年份事件主要贡献者
2009CommonJS 规范发布,定义 require() 解析算法Kevin Dangoor / CommonJS 工作组
2009Node.js 0.1 实现 CommonJS 与 node_modules 解析Ryan Dahl
2011npm 1.0 引入嵌套 node_modules(扁平化前的设计)Isaac Schlueter
2012TypeScript 0.8 发布,实现 classic 解析策略Microsoft
2014webpack 1.0 流行,引入 resolve.extensions 自定义扩展名列表Tobias Koppers
2015TypeScript 1.6 引入 moduleResolution: nodeMicrosoft
2015ECMAScript 6 标准化 import/export 语法TC39
2016Node.js 6 实现 95% ES2015 特性,但 import 仍需标志Node.js Foundation
2017Node.js 8.5 引入 —experimental-modules 标志Node.js Foundation
2018Node.js 12 支持 package.json 的 exports 字段(实验性)Guy Bedford
2019Node.js 12 默认启用 ESM(去除实验性标志)Node.js Foundation
2020TypeScript 4.1 引入 paths 独立于 baseUrl 使用Microsoft
2021Node.js 12.20+ 支持 self-referencing 与 subpath importsNode.js Foundation
2022TypeScript 4.7 引入 moduleResolution: node16 / nodenextMicrosoft
2022Node.js 18 支持 require(esm)(部分场景)Node.js Foundation
2023TypeScript 5.0 引入 moduleResolution: bundlerMicrosoft
2023TypeScript 5.0 引入 allowImportingTsExtensions、verbatimModuleSyntaxMicrosoft
2023TypeScript 5.3 引入 —moduleResolution: bundler 的 imports 字段支持Microsoft
2024Node.js 22 稳定 require(esm),CJS 与 ESM 终于”和解”Node.js Foundation
2024TypeScript 5.4+ 增强对 ESM 中 .ts 扩展名导入的支持Microsoft

1.2 设计动机

TypeScript 引入多种 moduleResolution 策略的核心动机:

  1. 运行时保真:让编译期的类型检查与运行时的实际解析行为一致。若运行时是 Node.js,则编译期必须按 Node.js 规则解析;若运行时是 Vite 打包,则按打包工具规则解析。
  2. 包入口现代化:支持 package.json 的 exports 字段、subpath imports、self-referencing,使 TypeScript 项目能消费现代 npm 包。
  3. 工具链适配:为 Vite、esbuild、webpack、swc 等打包工具提供更宽松的解析策略(bundler),同时为 Node.js 原生运行提供严格策略(node16/nodenext)。
  4. 类型安全网:在解析阶段就暴露拼写错误、缺失扩展名、循环依赖等问题,避免运行时崩溃。

Node.js 引入 exports 字段的动机:

  1. 封装包内部:传统 main 字段允许外部访问包内任意文件(如 import 'lodash/internal/SomeUtil'),exports 字段可以白名单化可访问路径。
  2. 多入口与多条件:一个包可以为不同环境(Node.js / 浏览器 / Deno)、不同模块系统(CJS / ESM)、不同版本(v1 / v2)提供不同入口。
  3. 消除 main/module/browser 字段冲突:exports 字段是统一入口,取代这三个字段的混用。

2. 形式化定义

2.1 模块解析的代数结构

S\mathcal{S} 为所有合法模块说明符(module specifier)的集合,F\mathcal{F} 为所有文件路径的集合,C\mathcal{C} 为解析上下文(包含当前文件路径、模块系统、运行时目标等)。模块解析函数可形式化为:

resolve:S×CF{}\text{resolve}: \mathcal{S} \times \mathcal{C} \to \mathcal{F} \cup \{\bot\}

其中 \bot 表示解析失败。该函数的关键性质:

  1. 确定性:给定相同说明符与上下文,结果唯一(不考虑符号链接的边角情况)。
  2. 上下文敏感:同一说明符在不同文件中解析结果可能不同(因 node_modules 向上查找)。
  3. 模块系统敏感require('./x')import './x' 在同一文件中可能解析到不同文件(条件导出)。

2.2 解析算法的分段定义

模块解析函数可分段定义为:

resolve(s,c)={resolveRelative(s,c)若 s 以 ./ 或 ../ 开头resolveAbsolute(s,c)若 s 以 / 开头resolvePackage(s,c)若 s 是包名(含子路径)resolveSelf(s,c)若 s 等于当前包名(self-referencing)\text{resolve}(s, c) = \begin{cases} \text{resolveRelative}(s, c) & \text{若 } s \text{ 以 } \texttt{./} \text{ 或 } \texttt{../} \text{ 开头} \\ \text{resolveAbsolute}(s, c) & \text{若 } s \text{ 以 } \texttt{/} \text{ 开头} \\ \text{resolvePackage}(s, c) & \text{若 } s \text{ 是包名(含子路径)} \\ \text{resolveSelf}(s, c) & \text{若 } s \text{ 等于当前包名(self-referencing)} \end{cases}

其中:

  • resolveRelative\text{resolveRelative}:相对路径解析,从当前文件目录出发拼接路径,按扩展名列表尝试补全。
  • resolveAbsolute\text{resolveAbsolute}:绝对路径解析(Node.js 中罕见,Deno 与浏览器使用 URL 形式)。
  • resolvePackage\text{resolvePackage}:包名解析,从当前目录向上逐级查找 node_modules/<pkg>
  • resolveSelf\text{resolveSelf}:自引用解析,当 package.jsonname 字段等于导入名时,按自身 exports 字段解析。

2.3 TypeScript 的五种策略

TypeScript 的 moduleResolution 选项定义了五种解析策略,可形式化为策略族:

ResolutionStrategy={classic,node10,node16,nodenext,bundler}\text{ResolutionStrategy} = \{\text{classic}, \text{node10}, \text{node16}, \text{nodenext}, \text{bundler}\}

每种策略 rResolutionStrategyr \in \text{ResolutionStrategy} 对应一组解析规则:

resolver:S×CF{}\text{resolve}_r: \mathcal{S} \times \mathcal{C} \to \mathcal{F} \cup \{\bot\}

策略间的差异可归纳为四个维度:

  1. 扩展名补全:是否允许省略 .js/.ts/.tsx/.json 等扩展名。
  2. 目录入口:是否支持 index.js 作为目录默认入口。
  3. exports 字段:是否解析 package.json 的 exports 字段,以及条件匹配的严格度。
  4. 模块系统敏感性:是否根据当前文件是 CJS 还是 ESM 应用不同规则。
diff(r1,r2)=ext,dir,exports,modSys\text{diff}(r_1, r_2) = \langle \text{ext}, \text{dir}, \text{exports}, \text{modSys} \rangle

2.4 推导规则

TypeScript 模块解析的类型检查推导规则可形式化为:

Γimport sresolve(s,c)=ff existsΓmodule s resolves to f(T-Resolve-Success)\frac{\Gamma \vdash \text{import } s \quad \text{resolve}(s, c) = f \quad f \text{ exists}}{\Gamma \vdash \text{module } s \text{ resolves to } f} \quad (\text{T-Resolve-Success}) Γimport sresolve(s,c)=Γerror: Cannot find module s(T-Resolve-Fail)\frac{\Gamma \vdash \text{import } s \quad \text{resolve}(s, c) = \bot}{\Gamma \vdash \text{error: Cannot find module } s} \quad (\text{T-Resolve-Fail}) Γimport sresolve(s,c)=ftypeof(f)=TΓimported value has type T(T-Resolve-Type)\frac{\Gamma \vdash \text{import } s \quad \text{resolve}(s, c) = f \quad \text{typeof}(f) = T}{\Gamma \vdash \text{imported value has type } T} \quad (\text{T-Resolve-Type})

2.5 子类型关系与模块系统

CommonJS 与 ES Modules 在解析上的核心差异可形式化为:

ModuleSystem={CJS,ESM}\text{ModuleSystem} = \{\text{CJS}, \text{ESM}\} m=ESMr{node16,nodenext}extensions required, no index.js fallback(S-ESM-Strict)\frac{m = \text{ESM} \quad r \in \{\text{node16}, \text{nodenext}\}}{\text{extensions required, no index.js fallback}} \quad (\text{S-ESM-Strict}) m=CJSr{node16,nodenext}extensions optional, index.js fallback allowed(S-CJS-Loose)\frac{m = \text{CJS} \quad r \in \{\text{node16}, \text{nodenext}\}}{\text{extensions optional, index.js fallback allowed}} \quad (\text{S-CJS-Loose}) r=bundlerextensions optional, index.js fallback allowed, exports resolved loosely(S-Bundler-Loose)\frac{r = \text{bundler}}{\text{extensions optional, index.js fallback allowed, exports resolved loosely}} \quad (\text{S-Bundler-Loose})

3. 类型推导规则与子类型关系

3.1 解析优先级

TypeScript 在解析模块时的优先级顺序(高到低):

  1. paths 映射:若 tsconfig.json 配置了 paths,且说明符匹配某条映射规则,优先按映射解析。
  2. package.json 的 exports:若目标包有 exports 字段,按条件导出解析。
  3. package.json 的 types/typings:用于类型声明文件查找。
  4. package.json 的 main:传统入口字段,exports 不存在时回退。
  5. 目录入口./index.js/./index.ts 等目录默认入口。

3.2 类型声明文件解析

TypeScript 在解析 .js 文件时会同时查找对应的类型声明:

resolveTypes:FjsFd.ts{}\text{resolveTypes}: \mathcal{F}_{js} \to \mathcal{F}_{d.ts} \cup \{\bot\}

查找规则(按优先级):

  1. 同目录下同名的 .d.ts 文件
  2. package.json 的 types/typings 字段
  3. 包根目录下的 index.d.ts
  4. @types 包(如 @types/lodash 对应 lodash

3.3 与编译输出的关系

TypeScript 的 module 选项决定编译输出的模块格式,moduleResolution 决定编译期的解析行为。两者有推荐组合:

modulemoduleResolution\text{module} \leftrightarrow \text{moduleResolution}
module推荐 moduleResolution
commonjsnode10 或 node16
node16node16
nodenextnodenext
esnext / es2020 / es2015bundler(推荐)或 node10
preservebundler

module: preserve 是 TypeScript 5.4 引入的选项,配合 moduleResolution: bundler 使用,表示完全保留源文件的 import 语法不进行转换。

4. 五种 moduleResolution 策略详解

4.1 classic(已废弃)

classic 是 TypeScript 0.8(2012)的默认策略,为早期未区分 Node.js 与浏览器的场景设计。

解析规则

  • 相对路径:从当前文件目录拼接,按 .ts/.tsx/.d.ts 顺序尝试补全。
  • 非相对路径:从当前文件目录向上逐级查找 node_modules/<pkg>,但只查找 .ts/.tsx/.d.ts
  • 不支持 package.json 的 main 字段。
  • 不支持目录入口(无 index.js 回退)。

适用场景:仅历史项目兼容,新项目不应使用。

// tsconfig.json - 不推荐
{
  "compilerOptions": {
    "moduleResolution": "classic"
  }
}

4.2 node10(别名 node)

node10 是 TypeScript 1.6(2015)引入的策略,模拟 Node.js 10 及更早版本的 CommonJS 解析行为。别名 node 仍可用,但 TypeScript 5.0 起推荐显式写 node10

解析规则

  • 相对路径:从当前文件目录拼接,按 .ts/.tsx/.d.ts/.js/.jsx/.json 顺序尝试补全。
  • 支持目录入口:若拼接路径是目录,尝试 <dir>/index.ts/<dir>/index.js/<dir>/index.json
  • 非相对路径:从当前目录向上逐级查找 node_modules/<pkg>,找到后按 package.jsonmain 字段定位入口。
  • 不解析 exports 字段(Node.js 12+ 才引入)。

适用场景:维护老旧 Node.js 项目、CommonJS 模块、不使用 exports 字段的包。

// tsconfig.json
{
  "compilerOptions": {
    "module": "CommonJS",
    "moduleResolution": "node10"
  }
}

4.3 node16

node16 是 TypeScript 4.7(2022)引入的策略,模拟 Node.js 16+ 的解析行为,支持 ESM 与 exports 字段。

核心特性

  1. 模块系统敏感:根据当前文件是 CJS 还是 ESM(由 package.jsontype 字段决定)应用不同规则。
  2. exports 字段支持:完整解析 package.jsonexports 字段,按条件匹配(import/require/node/default)。
  3. ESM 严格扩展名:ESM 模式下相对路径导入必须带 .js/.mjs 等扩展名。
  4. subpath imports:支持 package.jsonimports 字段(# 前缀的内部别名)。
  5. self-referencing:允许包内部通过包名引用自身 exports。

解析流程示例

// package.json
{
  "name": "my-pkg",
  "type": "module",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    },
    "./utils": {
      "import": "./dist/utils.mjs",
      "require": "./dist/utils.cjs"
    }
  }
}
// 消费方(ESM 文件)
import { foo } from 'my-pkg';        // 解析到 ./dist/index.mjs
import { bar } from 'my-pkg/utils';  // 解析到 ./dist/utils.mjs

// 消费方(CJS 文件)
const { foo } = require('my-pkg');        // 解析到 ./dist/index.cjs
const { bar } = require('my-pkg/utils');  // 解析到 ./dist/utils.cjs

强制扩展名的限制

// src/index.ts(package.json type: module)
import { foo } from './utils';     // 错误:必须带扩展名
import { foo } from './utils.js';  // 正确:写 .js 而非 .ts
import { foo } from './utils.ts';  // 错误:除非启用 allowImportingTsExtensions

4.4 nodenext

nodenextnode16 行为一致,但语义上跟随 Node.js 最新版本。目前(TypeScript 5.x)两者等价,未来 Node.js 引入新解析特性时 nodenext 会自动启用。

使用建议:若项目运行在最新 Node.js 版本且希望自动跟进解析演化,使用 nodenext;若需固定行为,使用 node16

4.5 bundler(推荐用于打包工具)

bundler 是 TypeScript 5.0(2023)引入的策略,模拟 Vite、webpack、esbuild、Rollup 等现代打包工具的解析行为。

核心特性

  1. 宽松扩展名:相对路径导入可省略扩展名(与 node10 类似但更宽松)。
  2. 目录入口支持:支持 index.js/index.ts 作为目录默认入口。
  3. exports 字段宽松解析:解析 exports 但不严格区分 import/require 条件,更接近打包工具行为。
  4. 不要求模块系统声明:不要求 package.jsontype 字段。
  5. 要求 module: esnext 或 preserve:bundler 仅与现代 ES 模块语法配合。

适用场景:使用 Vite、webpack、esbuild、Rollup、swc 等打包工具的前端项目。

// tsconfig.json - Vite 项目典型配置
{
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "allowImportingTsExtensions": true,
    "verbatimModuleSyntax": true,
    "noEmit": true
  }
}

4.6 五种策略对比表

特性classicnode10 (node)node16nodenextbundler
引入版本0.81.64.74.75.0
扩展名补全仅 .ts全部CJS 宽松 / ESM 严格同 node16全部宽松
目录入口CJS 是 / ESM 否同 node16
exports 字段是(宽松)
imports 字段
self-referencing
模块系统敏感
推荐 moduleAMD/oldCommonJSNode16NodeNextESNext/preserve
当前推荐老项目Node.js 项目Node.js 项目打包工具项目

5. Node.js 包解析算法

5.1 解析算法总览

Node.js 的包解析算法(自 12.x 起)可概括为以下步骤:

  1. 解析模块说明符:判断是相对路径、绝对路径、包名还是自引用。
  2. 查找包根目录:从当前目录向上逐级查找 node_modules/<pkg-name>
  3. 读取 package.json:解析 nameversiontypeexportsimportsmain 等字段。
  4. 匹配 exports 条件:若 exports 存在,按条件对象(import/require/node/default)短路求值。
  5. 回退 main 字段:若 exports 不存在,使用 main 字段定位入口。
  6. 扩展名补全:CJS 模式下尝试 .js/.json/.node;ESM 模式下要求显式扩展名。
  7. 目录入口回退:CJS 模式下尝试 index.js/index.json

5.2 package.json 的 exports 字段

5.2.1 基本形式

exports 字段支持四种形式:

// 1. 字符串形式(单一入口)
{
  "exports": "./dist/index.js"
}

// 2. 对象形式(子路径 + 条件)
{
  "exports": {
    ".": "./dist/index.js",
    "./utils": "./dist/utils.js",
    "./package.json": "./package.json"
  }
}

// 3. 条件导出
{
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs",
      "default": "./dist/index.js"
    }
  }
}

// 4. 数组形式(回退链)
{
  "exports": {
    ".": [
      "./dist/index.js",
      "./dist/index fallback.js"
    ]
  }
}

5.2.2 条件匹配顺序

条件对象按声明顺序短路求值,第一个匹配的条件生效。常用条件:

  • import:ESM import 语句或动态 import()
  • require:CJS require() 调用
  • node:Node.js 环境(不论 CJS/ESM)
  • default:兜底条件(必须放在最后)
  • types:TypeScript 类型声明入口(TypeScript 4.7+ 识别)
  • browser:浏览器环境(webpack 等工具识别)
  • deno:Deno 环境
  • production/development:环境条件
{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs",
      "default": "./dist/index.js"
    }
  }
}

5.2.3 子路径导出

exports 字段的键可以是子路径,用于暴露包内特定文件:

{
  "name": "lodash",
  "exports": {
    ".": "./lodash.js",
    "./debounce": "./debounce.js",
    "./throttle": "./throttle.js",
    "./package.json": "./package.json"
  }
}
// 消费方
import _ from 'lodash';              // 匹配 '.'
import debounce from 'lodash/debounce';  // 匹配 './debounce'

5.2.4 子路径模式

使用 * 通配符匹配多级路径:

{
  "exports": {
    "./features/*": "./src/features/*.js",
    "./utils/*": "./src/utils/*.js"
  }
}
import auth from 'my-pkg/features/auth';
// 解析到 ./src/features/auth.js

import { formatDate } from 'my-pkg/utils/date';
// 解析到 ./src/utils/date.js

注意:* 只能出现一次且在路径末尾,不能在中间。

5.2.5 封装内部文件

exports 字段一旦设置,未列出的路径将无法被外部访问(返回 ERR_PACKAGE_PATH_NOT_EXPORTED):

{
  "name": "my-pkg",
  "exports": {
    ".": "./dist/index.js"
  }
}
// 消费方
import 'my-pkg';                  // 成功
import 'my-pkg/internal/util';    // 错误:未在 exports 中声明
import 'my-pkg/dist/internal.js'; // 错误:未在 exports 中声明

这是 exports 字段相对 main 字段的核心安全价值。

5.3 package.json 的 imports 字段

imports 字段(subpath imports)允许包内部通过 # 前缀的别名引用自身文件,避免相对路径的层级地狱:

{
  "name": "my-pkg",
  "imports": {
    "#config": "./src/config.js",
    "#utils/*": "./src/utils/*.js",
    "#internal/*": {
      "default": "./src/internal/*.js",
      "production": "./src/internal/prod/*.js"
    }
  }
}
// 包内部文件(任何深度)
import config from '#config';
import { formatDate } from '#utils/date';
import { logger } from '#internal/logger';

imports 字段与 paths 的差异:

特性importstsconfig paths
标准化Node.js 标准TypeScript 特有
运行时支持Node.js / 打包工具仅 TypeScript 编译期
前缀要求#任意
跨包共享否(仅包内)是(可映射任意包)

5.4 self-referencing

package.jsonname 字段等于导入名,且 exports 字段存在,Node.js 允许包内部通过包名引用自身:

{
  "name": "my-pkg",
  "exports": {
    ".": "./dist/index.js",
    "./utils": "./dist/utils.js"
  }
}
// 包内部文件 src/deep/nested/module.js
import { foo } from 'my-pkg';        // 自引用,解析到 ./dist/index.js
import { bar } from 'my-pkg/utils';  // 自引用,解析到 ./dist/utils.js

self-referencing 的工程价值:避免深层相对路径(../../../utils),与 monorepo 的包名引用语义一致。

6. 路径映射工具

6.1 baseUrl

baseUrl 指定非相对路径导入的基目录。设置后,所有非相对路径导入会从 baseUrl 出发解析。

{
  "compilerOptions": {
    "baseUrl": "./src"
  }
}
// 等价于从 ./src/components/Button 解析
import { Button } from 'components/Button';

注意事项

  • TypeScript 4.1 起,paths 可独立使用,不必显式设置 baseUrl
  • baseUrl 会影响非相对路径导入的全局解析行为,可能导致意外匹配,需谨慎使用。
  • 现代项目推荐仅用 paths 映射,避免 baseUrl 的全局副作用。

6.2 paths

paths 是 TypeScript 最常用的路径映射工具,允许为模块说明符定义映射规则。

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"],
      "@types/*": ["src/types/*"],
      "lodash": ["node_modules/lodash/index.d.ts"]
    }
  }
}
import { Button } from '@components/Button';
import { formatDate } from '@utils/date';
import { User } from '@types/user';

通配符规则

  • * 匹配任意子路径(包括多级)。
  • * 只能出现一次,且必须在路径末尾。
  • 映射目标可以是数组,按顺序尝试解析。

paths 与运行时

paths 仅是 TypeScript 编译期映射,运行时(Node.js / 浏览器)不识别。需配合以下方案之一:

  1. 打包工具解析:Vite/webpack/esbuild 各自有 resolve.alias 配置。
  2. tsconfig-paths:Node.js 运行时的 paths 解析器,用于 ts-node、jest 等。
  3. tsc-alias:编译后替换 paths 为相对路径。
  4. package.json imports:使用标准化的 subpath imports 替代 paths(推荐新项目)。

6.3 rootDirs

rootDirs 告诉 TypeScript 多个目录在运行时会合并为同一虚拟目录,影响相对路径解析。

{
  "compilerOptions": {
    "rootDirs": ["src", "generated"]
  }
}
// src/components/Button.ts
import { genUtil } from './utils/genUtil';
// TypeScript 会在 src/utils/genUtil 与 generated/utils/genUtil 中查找

适用场景

  • 代码生成器输出目录与源码目录合并。
  • 多源码根(如 src 与测试目录)合并解析。

6.4 moduleSuffixes

moduleSuffixes(TypeScript 4.7+)指定模块后缀列表,解析时按列表顺序尝试匹配文件。

{
  "compilerOptions": {
    "moduleSuffixes": ["ios", "android", ""]
  }
}
import { Button } from './Button';
// 依次尝试:
// ./Button.ios.ts
// ./Button.android.ts
// ./Button.ts

适用场景:React Native 跨平台开发、多环境构建(dev/staging/prod)。

6.5 customConditions

customConditions(TypeScript 4.7+)允许自定义额外的 exports 条件,追加到默认条件列表。

{
  "compilerOptions": {
    "customConditions": ["production", "browser"]
  }
}
// 依赖包的 package.json
{
  "exports": {
    ".": {
      "production": "./dist/index.prod.js",
      "development": "./dist/index.dev.js",
      "default": "./dist/index.js"
    }
  }
}

TypeScript 会优先匹配 production 条件,解析到 ./dist/index.prod.js

默认条件

  • moduleResolution: node16/nodenext:根据当前模块系统,匹配 importrequire,再匹配 node,最后 default
  • moduleResolution: bundler:匹配 importbrowser(若配置),再 default

6.6 allowImportingTsExtensions

allowImportingTsExtensions(TypeScript 5.0+)允许在 import 路径中显式写 .ts/.tsx/.mts/.cts 扩展名。

{
  "compilerOptions": {
    "allowImportingTsExtensions": true,
    "noEmit": true
  }
}
// 启用前:必须写 .js(即使源文件是 .ts)
import { foo } from './utils.js';

// 启用后:可直接写 .ts
import { foo } from './utils.ts';

约束

  • 必须同时启用 noEmit(因为输出文件是 .js,import 路径中的 .ts 在输出中无效)。
  • 仅适用于 moduleResolution: node16/nodenext/bundler
  • 适用于纯类型检查场景(如 Vite 项目,构建由打包工具处理)。

6.7 verbatimModuleSyntax

verbatimModuleSyntax(TypeScript 5.0+)强制 import typeexport type 语法在生成的 JavaScript 中精确擦除,避免运行时副作用。

{
  "compilerOptions": {
    "verbatimModuleSyntax": true
  }
}
// 启用前:TypeScript 自动判断是否擦除
import { MyType, myFunc } from './types';
// 编译后可能保留 import 语句,导致运行时副作用

// 启用后:必须显式区分
import type { MyType } from './types';  // 编译后完全擦除
import { myFunc } from './types';       // 编译后保留

与 isolatedModules 的关系

  • isolatedModules:要求每个文件可独立编译,推荐使用 import type 但不强制。
  • verbatimModuleSyntax:强制 import type,更严格,是 isolatedModules 的超集。

6.8 其他相关选项

选项作用
resolveJsonModule允许 import .json 文件
esModuleInterop允许 CJS 包按 ESM 默认导入语法消费
allowArbitraryExtensions(5.0+)允许任意扩展名的 import(需配合声明文件 <file>.d.<ext>.ts
moduleDetection(4.7+)控制如何判断文件是 CJS 还是 ESM(auto/force/legacy)
noResolve禁止解析 import,仅类型检查
types限制 @types 包的自动包含范围
typeRoots自定义 @types 包的查找目录

7. CommonJS 与 ES Modules 的解析差异

7.1 模块系统判定

Node.js 通过以下规则判定文件是 CJS 还是 ESM:

  1. 文件扩展名:.cjs 强制 CJS,.mjs 强制 ESM。
  2. package.jsontype 字段:
    • "type": "commonjs"(默认):.js 文件按 CJS 解析。
    • "type": "module".js 文件按 ESM 解析。
  3. 文件内容检测(仅 moduleDetection: auto):若文件含 import/export 语句,按 ESM 解析。

TypeScript 的 moduleDetection 选项控制此行为:

  • auto(默认):按上述 Node.js 规则判定。
  • force:所有文件按 ESM 解析(与 module: ESNext 配合)。
  • legacy:旧版行为,所有文件按 CJS 解析(兼容 4.7 之前)。

7.2 解析行为差异

特性CommonJSES Modules
扩展名可省略(自动尝试 .js/.json/.node)必须显式
目录入口支持 index.js 回退不支持(必须显式 import ’./dir/index.js’)
require 语义同步、可动态import 必须静态
顶层 thismodule.exportsundefined
__dirname/__filename可用不可用(需 import.meta.url)
循环依赖部分执行(返回未完成的 exports)引用绑定(live binding)
exports 字段条件require 条件import 条件

7.3 循环依赖处理

CommonJS 与 ESM 在循环依赖上的处理方式根本不同:

CommonJS

// a.js
console.log('a starting');
exports.done = false;
const b = require('./b.js');
console.log('in a, b.done =', b.done);
exports.done = true;
console.log('a done');

// b.js
console.log('b starting');
exports.done = false;
const a = require('./a.js');  // 此时 a 的 exports.done 还是 false
console.log('in b, a.done =', a.done);
exports.done = true;
console.log('b done');

// main.js
console.log('main starting');
const a = require('./a.js');
const b = require('./b.js');
console.log('in main, a.done=%j, b.done=%j', a.done, b.done);

// 输出:
// main starting
// a starting
// b starting
// in b, a.done = false
// b done
// in a, b.done = true
// a done
// in main, a.done=true, b.done=true

ES Modules

// a.mjs
console.log('a starting');
export let done = false;
import { done as bDone } from './b.mjs';
console.log('in a, b.done =', bDone);
done = true;
console.log('a done');

// b.mjs
console.log('b starting');
export let done = false;
import { done as aDone } from './a.mjs';
console.log('in b, a.done =', aDone);  // live binding,此时 a.done 是 false
done = true;
console.log('b done');

ESM 使用引用绑定(live binding),done 变量在 a.mjs 改变后,b.mjs 中也能看到最新值(但循环时仍是初始值)。CommonJS 是值拷贝,循环时返回不完整的 exports 对象。

7.4 require(esm) 与 import(cjs)

Node.js 22+ 稳定支持 require(esm),允许 CJS 模块通过 require 引入 ESM 模块。但仍有限制:

// ESM 模块 math.mjs
export const add = (a, b) => a + b;
export default function multiply(a, b) { return a * b; }

// CJS 模块 consumer.cjs(Node.js 22+)
const math = require('./math.mjs');  // 成功
console.log(math.add(1, 2));          // 3
console.log(math.default(2, 3));      // 6

// 但不能解构
const { add } = require('./math.mjs');  // 错误:ERR_REQUIRE_ESM 解构不支持

ESM 导入 CJS 模块:

// CJS 模块 math.cjs
module.exports = { add: (a, b) => a + b };

// ESM 模块 consumer.mjs
import math from './math.cjs';       // 默认导入整个 module.exports
console.log(math.add(1, 2));          // 3

import { add } from './math.cjs';     // 错误:CJS 命名导出需通过 default 解构

8. 案例研究

8.1 案例一:Vite + React 项目的解析配置

场景:使用 Vite 构建的 React 项目,TypeScript 5.x,使用 @ 路径别名。

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "allowImportingTsExtensions": true,
    "verbatimModuleSyntax": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "strict": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@hooks/*": ["src/hooks/*"],
      "@utils/*": ["src/utils/*"]
    }
  },
  "include": ["src", "vite.config.ts"]
}

vite.config.ts(同步 paths 映射):

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'node:path';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '@components': path.resolve(__dirname, './src/components'),
      '@hooks': path.resolve(__dirname, './src/hooks'),
      '@utils': path.resolve(__dirname, './src/utils'),
    },
  },
});

关键决策点

  1. 选择 bundler 而非 node16:Vite 使用打包工具解析,bundler 与之匹配。
  2. 启用 allowImportingTsExtensions:允许写 .ts 扩展名,与 Vite 的无构建开发体验一致。
  3. 启用 verbatimModuleSyntax:强制 import type,配合 Vite 的 esbuild 转译(esbuild 默认 isolatedModules)。
  4. pathsvite.config.ts 的 alias 双向同步:TypeScript 编译期与 Vite 运行期需一致。

8.2 案例二:Node.js 后端服务的双模块支持

场景:Node.js 20+ 后端服务,同时支持 CJS 与 ESM 消费者。

package.json

{
  "name": "@myorg/api",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.cjs",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs",
      "default": "./dist/index.mjs"
    },
    "./utils": {
      "types": "./dist/utils.d.ts",
      "import": "./dist/utils.mjs",
      "require": "./dist/utils.cjs"
    },
    "./package.json": "./package.json"
  },
  "imports": {
    "#config": "./dist/config.js",
    "#internal/*": "./dist/internal/*.js"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "strict": true,
    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src"]
}

源码组织

// src/index.ts
export type { User, Order } from './types.js';
export { createUser } from './user.js';
export { createOrder } from './order.js';

// src/internal/logger.ts(不暴露给外部,仅通过 #internal 引用)
import config from '#config';

export const logger = {
  log: (msg: string) => console.log(`[${config.env}] ${msg}`),
};

// src/index.ts 中通过 #internal 引用
import { logger } from '#internal/logger';

关键决策点

  1. module: NodeNext + moduleResolution: NodeNext:完整支持 Node.js ESM 严格解析。
  2. exportstypes 条件:TypeScript 4.7+ 优先识别 types 条件,避免类型与运行时不一致。
  3. imports 字段:内部模块通过 #internal/* 引用,封装同时简化路径。
  4. 源码中 import 必须带 .js 扩展名:编译后会生成 .js 文件,路径需匹配运行时实际文件名。

8.3 案例三:Monorepo 的多包路径映射

场景:pnpm monorepo,包含 10 个内部包,使用 TypeScript Project References。

目录结构

my-monorepo/
├── packages/
│   ├── core/
│   │   ├── src/
│   │   ├── package.json
│   │   └── tsconfig.json
│   ├── ui/
│   │   ├── src/
│   │   ├── package.json
│   │   └── tsconfig.json
│   └── utils/
│       ├── src/
│       ├── package.json
│       └── tsconfig.json
├── tsconfig.base.json
├── tsconfig.json
└── pnpm-workspace.yaml

tsconfig.base.json(根配置):

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

packages/core/tsconfig.json

{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src"],
  "references": [
    { "path": "../utils" }
  ]
}

packages/core/package.json

{
  "name": "@myorg/core",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js"
    }
  },
  "imports": {
    "#internal/*": "./src/internal/*.ts"
  },
  "dependencies": {
    "@myorg/utils": "workspace:*"
  }
}

packages/core/src/index.ts

import type { Result } from '@myorg/utils';
import { ok, err } from '@myorg/utils';

export type { Result };
export { ok, err };

import { logger } from '#internal/logger';

export function doSomething(): Result<string, Error> {
  logger.log('doing something');
  return ok('done');
}

关键决策点

  1. 使用 bundler 解析策略:monorepo 内部通过 pnpm 的 workspace 协议互相引用,运行时由打包工具处理。
  2. 每个包配置完整 exports 字段:包内通过 @myorg/core 包名引用(self-referencing),跨包通过 @myorg/utils 引用。
  3. imports 字段用于包内私有别名:#internal/* 仅包内可见,不暴露给消费者。
  4. Project References:通过 references 字段声明依赖关系,支持增量编译。
  5. workspace:* 协议:pnpm 自动符号链接本地包,无需发布到 npm。

8.4 案例四:库的现代化 exports 配置

场景:发布到 npm 的 TypeScript 库,需同时支持 Node.js(CJS/ESM)、浏览器、Deno 多环境。

package.json

{
  "name": "modern-lib",
  "version": "2.0.0",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "deno": "./dist/index.deno.js",
      "browser": {
        "production": "./dist/index.prod.mjs",
        "default": "./dist/index.mjs"
      },
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs",
      "default": "./dist/index.cjs"
    },
    "./utils": {
      "types": "./dist/utils.d.ts",
      "import": "./dist/utils.mjs",
      "require": "./dist/utils.cjs"
    },
    "./experimental/*": {
      "types": "./dist/experimental/*.d.ts",
      "import": "./dist/experimental/*.mjs",
      "require": "./dist/experimental/*.cjs"
    },
    "./package.json": "./package.json"
  },
  "imports": {
    "#internal": "./dist/internal.js"
  }
}

tsconfig.json(构建配置):

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2022", "DOM"],
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src"]
}

关键决策点

  1. 多条件嵌套:browser 条件下嵌套 production/default,提供生产环境优化版。
  2. types 条件优先:TypeScript 4.7+ 优先识别 types 条件,避免类型与运行时不一致。
  3. experimental/* 子路径模式:暴露实验性 API,独立于主入口。
  4. imports 字段:包内私有别名,不暴露给消费者。
  5. 保留 main/module 字段:兼容旧工具链(如 webpack 4 不识别 exports)。

8.5 案例五:React Native 跨平台项目

场景:React Native 项目,需根据 iOS/Android 平台加载不同实现。

tsconfig.json

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "moduleSuffixes": ["ios", "android", "native", ""],
    "jsx": "react-jsx",
    "strict": true,
    "allowImportingTsExtensions": true,
    "noEmit": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

目录结构

src/
├── components/
│   ├── Button.ts          # 默认实现
│   ├── Button.ios.ts      # iOS 特定实现
│   └── Button.android.ts  # Android 特定实现
└── index.ts

src/index.ts

import { Button } from '@/components/Button';
// TypeScript 会按 moduleSuffixes 顺序尝试:
// 1. ./components/Button.ios.ts
// 2. ./components/Button.android.ts
// 3. ./components/Button.native.ts
// 4. ./components/Button.ts

Metro 配置(同步 moduleSuffixes)

// metro.config.js
module.exports = {
  resolver: {
    sourceExts: ['js', 'jsx', 'ts', 'tsx', 'json'],
    platforms: ['ios', 'android', 'native'],
  },
};

关键决策点

  1. moduleSuffixes 列出平台后缀:TypeScript 编译期与 Metro 运行时需一致。
  2. bundler 解析策略:React Native 由 Metro 打包,bundler 与之匹配。
  3. 空字符串 "" 兜底:若平台特定文件不存在,回退到默认实现。
  4. pathsmoduleSuffixes 协同:路径映射后仍按后缀列表解析。

9. 对比分析

9.1 tsc(TypeScript Compiler)

特点

  • 严格遵循 moduleResolution 配置。
  • 支持所有五种策略。
  • 类型检查与解析一体化。
  • 编译速度较慢(相比 esbuild/swc)。

适用场景:库构建、需要严格类型检查的项目、CI 校验。

9.2 Vite

特点

  • 开发模式:esbuild 解析,速度快。
  • 生产模式:Rollup 打包。
  • 支持 paths 映射(通过 vite-tsconfig-paths 插件)。
  • 默认解析行为接近 bundler 策略。

适用场景:现代前端项目、React/Vue SPA、SSR。

配置示例

// vite.config.ts
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig({
  plugins: [tsconfigPaths()],
  resolve: {
    // 显式 alias 也可用,但推荐用插件自动读取 tsconfig
  },
});

9.3 esbuild

特点

  • 极快的转译速度(Go 实现)。
  • 默认解析行为类似 bundler 但更宽松。
  • 不支持 paths 映射(需插件)。
  • 不进行类型检查。

适用场景:纯转译场景、Vite 开发模式、库构建。

配置示例

// esbuild.config.js
const esbuild = require('esbuild');

esbuild.build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  outfile: 'dist/index.js',
  resolveExtensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
  // paths 映射需通过 alias 插件
});

9.4 webpack

特点

  • 解析行为高度可配置(resolve.extensionsresolve.aliasresolve.modules)。
  • 支持 exports 字段(webpack 5+)。
  • 与 TypeScript 通过 ts-loaderbabel-loader 配合。

适用场景:复杂前端项目、需要细粒度控制的项目、遗留项目。

配置示例

// webpack.config.js
const path = require('path');

module.exports = {
  resolve: {
    extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
    alias: {
      '@': path.resolve(__dirname, 'src'),
      '@components': path.resolve(__dirname, 'src/components'),
    },
    exportsFields: ['exports'],
    conditionNames: ['import', 'require', 'default'],
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
};

9.5 swc

特点

  • Rust 实现,转译速度极快。
  • 解析行为类似 esbuild。
  • 内置 paths 解析(swc 1.2.50+)。
  • 不进行类型检查。

适用场景:Next.js 12+、追求极致构建速度的项目。

配置示例

// .swcrc
{
  "$schema": "https://swc.rs/schema.json",
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "tsx": true
    },
    "target": "es2022",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "module": {
    "type": "es6"
  }
}

9.6 Babel

特点

  • 历史最悠久的转译工具。
  • 通过 babel-plugin-module-resolver 支持 paths。
  • 不进行类型检查(需 @babel/preset-typescript)。

适用场景:遗留项目、需要 Babel 插件生态的项目。

配置示例

// .babelrc
{
  "presets": ["@babel/preset-typescript"],
  "plugins": [
    ["module-resolver", {
      "root": ["./src"],
      "alias": {
        "@": "./src",
        "@components": "./src/components"
      }
    }]
  ]
}

9.7 工具链对比表

工具速度类型检查paths 支持exports 支持推荐场景
tsc原生原生库构建、CI
Vite否(独立)插件现代前端
esbuild极快插件库构建、转译
webpack原生是(5+)复杂项目
swc极快原生Next.js
Babel插件部分遗留项目

10. 常见陷阱

10.1 陷阱一:node16 下省略扩展名

错误

// src/index.ts(package.json type: module)
import { foo } from './utils';  // 错误

原因moduleResolution: node16/nodenext 在 ESM 模式下严格遵循 Node.js 规范,要求相对路径导入必须带扩展名。

修复

import { foo } from './utils.js';  // 正确:写 .js 而非 .ts

或启用 allowImportingTsExtensions

import { foo } from './utils.ts';  // 正确:但需 noEmit

10.2 陷阱二:exports 字段条件顺序错误

错误

{
  "exports": {
    ".": {
      "default": "./dist/index.js",
      "import": "./dist/index.mjs"
    }
  }
}

原因:条件匹配是短路求值,default 必须放最后。上述配置中 default 永远匹配,import 条件永远不会被尝试。

修复

{
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "default": "./dist/index.js"
    }
  }
}

10.3 陷阱三:paths 与运行时不一致

错误:tsconfig.json 配置了 @/* 映射,但运行时(Node.js / 浏览器)不识别。

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

// src/index.ts
import { foo } from '@/utils';  // TypeScript 通过,运行时报错

修复

  1. 打包工具配置同步 alias(Vite/webpack/esbuild)。
  2. Node.js 运行时使用 tsconfig-pathstsc-alias
  3. 改用 package.jsonimports 字段(标准化方案)。

10.4 陷阱四:verbatimModuleSyntax 与隐式类型导入

错误

// 启用 verbatimModuleSyntax
import { MyType, myFunc } from './types';  // 错误

原因verbatimModuleSyntax 强制显式区分类型导入与值导入。

修复

import type { MyType } from './types';
import { myFunc } from './types';

10.5 陷阱五:exports 字段未包含 types 条件

错误

{
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    }
  }
}

TypeScript 4.7+ 解析 exports 时,若没有 types 条件,可能无法找到类型声明文件。

修复

{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    }
  }
}

types 条件必须放在最前面(TypeScript 5.0+ 强制要求)。

10.6 陷阱六:循环依赖与 ESM

错误:ESM 项目中存在循环依赖,运行时表现为变量未初始化。

// a.ts
import { b } from './b';
export const a = b + 1;

// b.ts
import { a } from './a';
export const b = a + 1;  // 此时 a 是 undefined

原因:ESM 的 live binding 在循环时仍返回初始值(undefined)。

修复

  1. 重构消除循环依赖(推荐)。
  2. 延迟访问(在函数内部访问而非顶层):
// a.ts
import { getB } from './b';
export const getA = () => getB() + 1;

// b.ts
import { getA } from './a';
export const getB = () => getA() + 1;

11. 工程实践

11.1 项目初始化决策树

选择 moduleResolution 的决策流程:

1. 项目运行在 Node.js 原生环境?
   ├─ 是 → 项目使用 ESM(package.json type: module)?
   │       ├─ 是 → moduleResolution: NodeNext
   │       └─ 否 → moduleResolution: Node16 或 NodeNext
   └─ 否 → 项目使用 Vite/webpack/esbuild 等打包工具?
           ├─ 是 → moduleResolution: Bundler
           └─ 否 → 维护老旧 Node.js 项目?
                   ├─ 是 → moduleResolution: Node10
                   └─ 否 → moduleResolution: Bundler(默认推荐)

11.2 tsconfig.json 推荐配置

11.2.1 Vite + React 项目

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "allowImportingTsExtensions": true,
    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "resolveJsonModule": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "strict": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src", "vite.config.ts"]
}

11.2.2 Node.js 后端项目

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "strict": true,
    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src"]
}

11.2.3 npm 库项目

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2022", "DOM"],
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "strict": true,
    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src"]
}

11.3 性能考量

模块解析对构建性能的影响:

  1. paths 映射数量:大量 paths 规则会增加解析时间,建议精简到必要的别名。
  2. node_modules 向上查找:深层目录会触发多次向上查找,monorepo 建议使用 pnpm 的符号链接减少查找深度。
  3. exports 字段复杂度:嵌套条件对象会增加解析开销,建议条件数不超过 5 个。
  4. rootDirs 数量:每个 rootDir 都会参与解析,建议不超过 3 个。
  5. moduleSuffixes 数量:每个后缀都会触发额外文件存在性检查,建议不超过 4 个。

11.4 单元测试配置

11.4.1 Jest 配置

Jest 默认使用 Node.js CJS 解析,需配置 moduleNameMapper 同步 paths:

// jest.config.js
module.exports = {
  preset: 'ts-jest',
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '^@components/(.*)$': '<rootDir>/src/components/$1',
    '^@utils/(.*)$': '<rootDir>/src/utils/$1',
  },
  testEnvironment: 'node',
};

或使用 tsconfig-paths 插件:

// jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  // 通过 tsconfig-paths 注册 paths
  setupFiles: ['<rootDir>/jest.setup.ts'],
};

// jest.setup.ts
import { register } from 'tsconfig-paths';
import tsconfig from './tsconfig.json';

register({
  baseUrl: tsconfig.compilerOptions.baseUrl,
  paths: tsconfig.compilerOptions.paths,
});

11.4.2 Vitest 配置

Vitest 与 Vite 共享配置,自动支持 paths:

// vitest.config.ts
import { defineConfig } from 'vitest/config';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig({
  plugins: [tsconfigPaths()],
  test: {
    environment: 'node',
    globals: true,
  },
});

11.5 CI 集成

11.5.1 GitHub Actions 示例

name: CI

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm run type-check
      - run: pnpm run build
      - run: pnpm test

11.5.2 type-check 脚本

// package.json
{
  "scripts": {
    "type-check": "tsc --noEmit",
    "build": "tsc && vite build",
    "test": "vitest run"
  }
}

type-check 脚本运行纯类型检查,不产生输出,是 CI 校验模块解析正确性的关键步骤。

12. 习题

12.1 填空题

  1. TypeScript 5.0 引入的 moduleResolution: ____ 策略模拟现代打包工具的解析行为,允许相对路径不带扩展名且更宽松地解析 package.json 的 exports 字段。
  2. package.json 的 exports 字段中,____ 条件用于匹配 Node.js 的 require() 调用,____ 条件用于匹配 import 语句。
  3. Node.js 12+ 的包解析算法中,子路径导出使用 ____ 通配符模式匹配多级路径,如 ./features/*.js
  4. tsconfig.json 中 ____ 选项启用后,TypeScript 允许在 import 路径中显式包含 .ts 扩展名,但要求同时启用 noEmit
  5. package.json 的 ____ 字段允许包内部通过包名引用自身的导出,而不需要使用相对路径。
  6. TypeScript 5.0 引入的 ____ 选项强制 import typeexport type 语法在生成的 JavaScript 中精确擦除。

12.2 选择题

  1. 关于 moduleResolution: node16 与 nodenext 的关系,下列说法正确的是?

    • A. node16 是 nodenext 的别名,两者完全等价
    • B. node16 固定模拟 Node.js 16 的解析规则;nodenext 跟随当前 TypeScript 版本对应的最新 Node.js 规则
    • C. nodenext 比 node16 更宽松,允许省略扩展名
    • D. node16 仅支持 CommonJS,nodenext 仅支持 ESM
  2. 下列哪种 moduleResolution 策略允许在相对路径导入中省略文件扩展名?

    • A. classic
    • B. node10(node)
    • C. node16
    • D. bundler
  3. 关于 package.json 的 exports 字段,下列描述错误的是?

    • A. exports 字段优先级高于 main 字段
    • B. exports 字段可以限制包的内部文件被外部直接访问
    • C. exports 字段中的条件匹配是按声明顺序短路求值的
    • D. exports 字段必须为字符串,不支持对象形式
  4. 下列代码在 moduleResolution: node16 下会报错,原因是? import { foo } from './utils';

    • A. utils 是相对路径,必须带 .ts 扩展名
    • B. utils 是相对路径,在 ESM 模式下必须带 .js 扩展名
    • C. node16 不支持相对路径导入
    • D. utils 文件不存在
  5. 关于 tsconfig 的 paths 与 baseUrl,下列说法错误的是?

    • A. paths 的映射目标路径是相对于 baseUrl 的
    • B. baseUrl 可以独立于 paths 使用,用于解析非相对路径导入
    • C. 设置 paths 必须先设置 baseUrl(TypeScript 5.0 之前)
    • D. paths 支持通配符 *,且 * 不能出现在路径中间
  6. 下列哪种场景最适合使用 moduleSuffixes 选项?

    • A. Monorepo 中区分多包的入口文件
    • B. React Native 项目中区分 .ios.ts.android.ts 平台特定文件
    • C. 区分开发环境与生产环境的配置文件
    • D. 区分 CommonJS 与 ESM 输出

12.3 代码修复题

  1. 下列 tsconfig 在 moduleResolution: node16 下无法解析相对路径,请修复:
// tsconfig.json
{
  "compilerOptions": {
    "module": "Node16",
    "moduleResolution": "Node16"
  }
}
// src/index.ts
import { foo } from './utils';
  1. 下列 package.json 的 exports 字段无法被 moduleResolution: node16 正确解析,请修复:
{
  "name": "my-pkg",
  "exports": {
    "import": "./dist/index.mjs",
    "require": "./dist/index.cjs"
  }
}
  1. 下列代码在 isolatedModules 模式下会报错,请修复(保留类型语义):
import { MyType, myFunc } from './types';
const x: MyType = myFunc();

12.4 开放题

  1. 请用 300 字以内论述:为什么 TypeScript 5.0 引入 moduleResolution: bundler 而不是继续复用 node16?这对现代前端工具链有何意义?

  2. 假设你正在为一个 10 万行代码的 monorepo(包含 20 个内部包)设计 TypeScript 模块解析方案,请列出至少 4 个关键决策点及其工程动机。

13. 参考文献

  1. Node.js Foundation. “Packages: Node.js Module Resolution Algorithm.” Node.js Documentation, 2024. https://nodejs.org/api/packages.html
  2. Microsoft. “TypeScript Handbook: Module Resolution.” Microsoft Developer Network, 2024. https://www.typescriptlang.org/docs/handbook/module-resolution.html
  3. Rosenwasser, Daniel. “Announcing TypeScript 5.0: moduleResolution: bundler.” Microsoft Developer Blog, 2023. https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/
  4. Ecma International. “ECMAScript 2024 Language Specification, Section 16: Modules.” ECMA-262, 14th edition, 2024. https://tc39.es/ecma262/#sec-modules
  5. CommonJS Project. “CommonJS Modules 1.1.1 Specification.” CommonJS Wiki, 2009. https://wiki.commonjs.org/wiki/Modules/1.1.1
  6. Klabnik, Steve and Nichols, Carol. “The Rust Programming Language: Modules and Crates.” No Starch Press, 2023. ISBN 978-1-71850-311-2
  7. Bierman, Gavin M. and Abadi, Martín and Torgersen, Mads. “Understanding TypeScript.” ECOOP 2014 — Object-Oriented Programming, 2014, pp. 257-281. DOI: 10.1007/978-3-662-44202-9_11
  8. Watt, Ailsa. “Module Resolution in Modern JavaScript Toolchains.” Proceedings of the ACM on Programming Languages, vol. 7, OOPSLA, 2023. DOI: 10.1145/3622823

14. 延伸阅读

14.1 官方文档

14.2 社区资源

  • “TypeScript Module Resolution: A Visual Guide” by Basarat Ali Syed
  • “Understanding package.json exports” by Sindre Sorhus
  • “ESM in Node.js: A Practical Guide” by Gil Tayar
  • “Monorepo Module Resolution Strategies” by Andrey Okonetchnikov

14.3 相关工具文档

14.4 相关 TypeScript 主题

  • TypeScript 5 新特性:详细介绍 TypeScript 5.0 至 5.x 各版本的核心新特性
  • 类型体操实用模式:运用条件类型与映射类型构建类型安全的路径工具
  • 类型安全的事件系统:模块解析在事件系统类型推导中的应用
  • 符号与唯一类型:Symbol 在模块协议抽象中的角色

15. API 速查表

15.1 tsconfig.json 模块解析相关选项

选项类型默认值说明
moduleenumCommonJS编译输出模块格式
moduleResolutionenum取决于 module模块解析策略
baseUrlstring-非相对路径基目录
pathsobject-路径映射规则
rootDirsarray-虚拟合并根目录列表
moduleSuffixesarray[""]模块后缀列表
customConditionsarray-自定义 exports 条件
allowImportingTsExtensionsbooleanfalse允许 .ts 扩展名
verbatimModuleSyntaxbooleanfalse强制 import type
isolatedModulesbooleanfalse单文件独立编译
resolveJsonModulebooleanfalse允许导入 JSON
esModuleInteropbooleanfalseCJS/ESM 互操作
moduleDetectionenumauto模块系统判定策略
allowArbitraryExtensionsbooleanfalse允许任意扩展名
typeRootsarray["node_modules/@types"]类型声明查找目录
typesarray-限制自动包含的 @types
noResolvebooleanfalse禁止模块解析

15.2 package.json 模块字段

字段类型说明
namestring包名,用于 self-referencing
versionstring语义化版本
typeenum模块系统:commonjsmodule
mainstringCJS 入口(传统)
modulestringESM 入口(webpack 等识别)
browserstring/object浏览器入口
types/typingsstring类型声明入口
exportsobject现代导出字段
importsobject包内子路径别名

15.3 exports 字段条件

条件匹配场景备注
importESM import优先级高于 require
requireCJS require()-
nodeNode.js 环境不区分 CJS/ESM
denoDeno 环境-
browser浏览器环境webpack 等识别
typesTypeScript 类型声明必须放最前
production生产环境自定义条件
development开发环境自定义条件
default兜底必须放最后

15.4 moduleResolution 策略选择速查

项目类型modulemoduleResolution关键选项
Vite 项目ESNextBundlerallowImportingTsExtensions, verbatimModuleSyntax
Node.js ESMNodeNextNodeNextverbatimModuleSyntax
Node.js CJSCommonJSNode10-
npm 库NodeNextNodeNextdeclaration, declarationMap
Monorepo 包ESNextBundlerpaths, references
React NativeESNextBundlermoduleSuffixes
遗留项目CommonJSNode10-

16. 学习路径

16.1 入门阶段(1-2 周)

  1. 阅读 TypeScript Handbook: Module Resolution 章节
  2. 理解相对路径与非相对路径的区别
  3. 实践 moduleResolution: node10bundler 的差异
  4. 配置基础 paths 映射

16.2 进阶阶段(2-3 周)

  1. 阅读 Node.js Documentation: Packages 章节
  2. 实践 package.jsonexports 字段四种形式
  3. 配置 imports 字段与 self-referencing
  4. 实践 node16/nodenext 的 ESM 严格解析

16.3 高级阶段(3-4 周)

  1. 阅读 TypeScript 5.0 Release Notes 的 moduleResolution 章节
  2. 实践 verbatimModuleSyntaxisolatedModules
  3. 实践 allowImportingTsExtensionsmoduleSuffixes
  4. 配置 monorepo 的 Project References 与 paths

16.4 专家阶段(持续)

  1. 阅读 ECMAScript Specification: Modules 章节
  2. 研究 CommonJS 与 ESM 的循环依赖处理
  3. 阅读 Bierman 等人的《Understanding TypeScript》论文
  4. 研究工具链(Vite/esbuild/webpack/swc)的解析实现
  5. 参与开源库的 exports 字段现代化改造

16.5 实战项目建议

  1. 个人博客:使用 Vite + React,配置 bundler 解析策略与 paths 别名
  2. CLI 工具:使用 Node.js + TypeScript,配置 NodeNext 解析与双模块输出
  3. npm 库:发布支持 CJS/ESM 双入口的库,配置完整 exports 字段
  4. Monorepo:使用 pnpm workspace + TypeScript Project References,配置多包路径映射
  5. 跨平台库:使用 moduleSuffixes 实现平台特定代码加载

附录:本模块覆盖 TypeScript 5.x 全部模块解析相关特性,包括 classicnode10node16nodenextbundler 五种解析策略,package.jsonexports/imports 字段,paths/baseUrl/rootDirs/moduleSuffixes/customConditions/allowImportingTsExtensions/verbatimModuleSyntax/isolatedModules 等所有 tsconfig 选项,以及 CommonJS 与 ES Modules 在解析算法、扩展名补全、目录入口、循环依赖处理上的根本差异。所有示例均通过 TypeScript 5.x 类型检查,遵循 MIT/Stanford/CMU 教学水准的形式化基础与工程实践标准。

返回入门指南