模块解析策略
本教程共 80 篇 · 第 56 篇 · 更新于 2026-08-10 · 约 10 分钟阅读
本节目标:理解 TypeScript 7.0 的模块解析机制——node16、nodenext、bundler 三种策略各有什么特点、路径解析遵循什么规则、以及 TS 7.0 移除 node10 后该如何选择。
模块解析(module resolution)就是编译器根据 import 语句里的字符串,找到对应的文件。听起来简单,实际涉及的规则不少——文件后缀、目录结构、package.json 配置、类型声明文件的位置,都影响解析结果。
TS 7.0 还有哪些策略
TypeScript 7.0 做了一次大清理:移除了 moduleResolution: "node10"。当前可用的三种策略:
| 策略 | 适用场景 |
|---|---|
node16 | 输出给 Node.js 16+ 运行的项目 |
nodenext | 同上,始终指向最新 Node.js 解析规则 |
bundler | 代码会经过打包工具处理的项目 |
node16 和 nodenext 在大多数情况下行为一致,区别在于 nodenext 会跟随 Node.js 的最新规范(比如未来新增的解析规则),而 node16 锁定在 Node 16 那套行为上。
bundler 是给打包工具(esbuild、webpack、Rollup、Vite)准备的。它假定你不需要在 Node.js 里直接 node 运行编译产物,因此放宽了一些限制。
WarningTS 7.0 移除了
moduleResolution: "node10"(旧称node)。如果你的tsconfig.json里还有这行,升级后直接报错。写成node16或nodenext,然后检查一下有没有导入路径需要补后缀名。
node16 / nodenext 策略
这两种策略忠实模拟 Node.js 的模块解析算法。对相对路径导入,规则如下:
// 相对路径——从当前文件位置开始找
import { add } from "./math.js";
import { render } from "../ui/button.js";
TypeScript 收到 "./math.js" 后会按顺序尝试:
./math.ts(源文件)./math.tsx./math.d.ts(类型声明文件)./math/index.ts(目录)./math/index.d.ts
对非相对路径(bare specifier),先去 node_modules 里找包的 package.json:
import express from "express";
解析步骤:
- 在
node_modules/express/package.json里找"exports"字段 - 如果有
"exports",按里面的映射规则解析 - 如果没有,找
"main"或"types"字段 - 都没有就找
node_modules/express/index.ts或index.d.ts
node16 和 nodenext 还有一个重要规则:必须携带文件扩展名。写 import "./math" 不带 .js 是不允许的(除非 ./math 是一个目录)。这看起来麻烦,但恰恰是现代 Node.js ESM 的要求,TypeScript 选择不遮掩这个问题。
// ❌ node16/nodenext 不允许
import { add } from "./math";
// ✅ 必须带扩展名
import { add } from "./math.js";
bundler 策略
打包工具的模块解析方式不一样——它们通常支持省略扩展名、路径别名、导入非 JS 资源(CSS、图片等)。bundler 策略模拟了这个环境:
// bundler 策略下这些都可以
import { add } from "./math"; // 省略扩展名
import logo from "./logo.png"; // 导入非 JS 资源
import styles from "./app.module.css"; // CSS Modules
bundler 策略不要求扩展名,也不检查 package.json 的 "exports" 字段——因为打包工具通常自己处理这些。
Note
bundler策略需要配合moduleResolution: "bundler"和对应的module设置一起用。如果你的tsconfig.json里module设为esnext或es2022,用bundler就很自然。
package.json 的类型相关字段
无论哪种解析策略,TypeScript 都会尊重 package.json 里的这些字段:
types / typings:指向这个包的 .d.ts 文件。
{
"name": "my-lib",
"main": "./dist/index.js",
"types": "./dist/index.d.ts"
}
当用户 import { foo } from "my-lib" 时,TypeScript 会去读 ./dist/index.d.ts 获取类型信息。
exports:Node.js 的模块映射字段,TypeScript 7.0 完全支持。
{
"name": "my-lib",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./helpers": {
"types": "./dist/helpers.d.ts",
"default": "./dist/helpers.js"
}
}
}
上面的配置允许用户写 import { foo } from "my-lib/helpers"。"types" 条件告诉 TypeScript 去哪个 .d.ts 文件找类型。打包工具和 Node.js 运行时则用 "default" 条件。
路径映射:baseUrl 和 paths
tsconfig.json 里可以配置路径别名,让你用简短的名字代替冗长的相对路径:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@utils/*": ["src/utils/*"],
"@components/*": ["src/components/*"]
}
}
}
这样写导入就清爽多了:
// 不用写成 ../../../utils/format
import { formatDate } from "@utils/format";
WarningTypeScript 只处理类型检查层面的路径映射,不会改写编译输出中的路径。你还需要在打包工具(或 Node.js 的
tsconfig-paths之类的工具)里配置同样的别名,运行时才能正确解析。
选哪种策略
给一个简单的决策思路:
- 你的代码用 Node.js 直接运行(
node dist/server.js)→ 选node16或nodenext,老实写.js扩展名 - 你的代码走 打包工具(Vite、webpack、esbuild、Rollup)→ 选
bundler,享受省略扩展名的便利 - 不确定?看你的
module字段:module: "esnext"配bundler,module: "node16"配node16
TS 7.0 的默认 module 是 esnext,但默认的 moduleResolution 仍然需要你在 tsconfig 里显式指定。推荐写法:
// 打包工具项目
{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler"
}
}
// Node.js 项目
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext"
}
}
小结
模块解析策略决定 TypeScript 如何从 import 路径找到文件。TS 7.0 提供三种策略:node16 和 nodenext 忠实模拟 Node.js 规则,要求带扩展名;bundler 为打包工具环境放宽限制。选对了策略,编辑器里的红线会少一大半。