TypeScript 模块解析(moduleResolution)详解
moduleResolution 决定 TS 如何定位 import 的模块。本文讲清 node10/node16/nodenext/bundler 四种策略的差异与选择。
2026 年的选择只有两种:打包器项目(Vite/webpack)用 "bundler",Node 直跑项目用 "NodeNext"。NodeNext 严格遵循 Node 的 ESM/CJS 规则(相对导入要带 .js 扩展名),bundler 更宽松。老配置(node10)只存在于存量项目。
模块解析是什么
import { x } from './utils' —— TS 需要找到 utils 到底是 ./utils.ts 还是 ./utils/index.ts。解析策略决定查找规则与对裸导入(npm 包)的处理。
四种策略
| 策略 | 适用 |
|---|---|
| node10(旧 node) | 存量 CJS 项目,不推荐新用 |
| node16 / nodenext | Node 直跑(ESM/CJS 按 Node 规则) |
| bundler | 打包器项目(Vite/webpack/tsx) |
// Node 项目
{ "module": "NodeNext", "moduleResolution": "NodeNext" }
// 打包器项目
{ "module": "ESNext", "moduleResolution": "bundler" }
NodeNext 的严格性
// ESM 模式下相对导入必须带扩展名(编译后的 .js)
import { helper } from './utils.js'; // 注意是 .js 不是 .ts
这是 Node ESM 的真实规则——TS 只是如实反映。用 tsx/bundler 的项目无此烦恼(bundler 模式允许省略)。
node_modules 解析
裸导入(import express from 'express')的查找:包的 exports 字段 → types 条件 → 主入口。pnpm 的严格结构下,只有声明为依赖的包可被解析——幻影依赖在解析层被拦截。
常见问题(FAQ)
Q:报错 "Cannot find module" 但文件明明在?
九成是 moduleResolution 与项目实际运行方式不匹配:打包器项目误用 NodeNext(缺扩展名报错)或反之。先确认代码最终怎么跑。
Q:paths 别名和解析策略的关系?
paths 是解析前的重写层——TS 侧生效;运行时需工具链配合(tsx/esbuild 自动,Node 原生需 imports 字段或 loader)。
Q:迁移老项目到 NodeNext 的成本?
主要工作:相对导入补 .js 扩展名(有 codemod)、默认导入 CJS 包的互操作检查。一次投入换来与 Node 语义严格一致。
参考文档
本文依据公开文档整理,示例需按项目依赖版本验证;不将文档中的性能描述视为本项目实测结果。