首页 / TypeScript 入门教程 / 模块解析策略

TypeScript 入门教程

模块解析策略

本教程共 80 篇 · 第 56 篇 · 更新于 2026-08-10 · 约 10 分钟阅读

TypeScriptTypeScript 入门教程模块解析moduleResolutionnode16nodenextbundler

本节目标:理解 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代码会经过打包工具处理的项目

node16nodenext 在大多数情况下行为一致,区别在于 nodenext 会跟随 Node.js 的最新规范(比如未来新增的解析规则),而 node16 锁定在 Node 16 那套行为上。

bundler 是给打包工具(esbuild、webpack、Rollup、Vite)准备的。它假定你不需要在 Node.js 里直接 node 运行编译产物,因此放宽了一些限制。

Warning

TS 7.0 移除了 moduleResolution: "node10"(旧称 node)。如果你的 tsconfig.json 里还有这行,升级后直接报错。写成 node16nodenext,然后检查一下有没有导入路径需要补后缀名。

node16 / nodenext 策略

这两种策略忠实模拟 Node.js 的模块解析算法。对相对路径导入,规则如下:

// 相对路径——从当前文件位置开始找
import { add } from "./math.js";
import { render } from "../ui/button.js";

TypeScript 收到 "./math.js" 后会按顺序尝试:

  1. ./math.ts(源文件)
  2. ./math.tsx
  3. ./math.d.ts(类型声明文件)
  4. ./math/index.ts(目录)
  5. ./math/index.d.ts

对非相对路径(bare specifier),先去 node_modules 里找包的 package.json

import express from "express";

解析步骤:

  1. node_modules/express/package.json 里找 "exports" 字段
  2. 如果有 "exports",按里面的映射规则解析
  3. 如果没有,找 "main""types" 字段
  4. 都没有就找 node_modules/express/index.tsindex.d.ts

node16nodenext 还有一个重要规则:必须携带文件扩展名。写 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.jsonmodule 设为 esnextes2022,用 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";
Warning

TypeScript 只处理类型检查层面的路径映射,不会改写编译输出中的路径。你还需要在打包工具(或 Node.js 的 tsconfig-paths 之类的工具)里配置同样的别名,运行时才能正确解析。

选哪种策略

给一个简单的决策思路:

  • 你的代码用 Node.js 直接运行node dist/server.js)→ 选 node16nodenext,老实写 .js 扩展名
  • 你的代码走 打包工具(Vite、webpack、esbuild、Rollup)→ 选 bundler,享受省略扩展名的便利
  • 不确定?看你的 module 字段:module: "esnext"bundlermodule: "node16"node16

TS 7.0 的默认 moduleesnext,但默认的 moduleResolution 仍然需要你在 tsconfig 里显式指定。推荐写法:

// 打包工具项目
{
  "compilerOptions": {
    "module": "esnext",
    "moduleResolution": "bundler"
  }
}

// Node.js 项目
{
  "compilerOptions": {
    "module": "nodenext",
    "moduleResolution": "nodenext"
  }
}

小结

模块解析策略决定 TypeScript 如何从 import 路径找到文件。TS 7.0 提供三种策略:node16nodenext 忠实模拟 Node.js 规则,要求带扩展名;bundler 为打包工具环境放宽限制。选对了策略,编辑器里的红线会少一大半。