告别 require!TypeScript 5.9 与 Node.js 20+ 的 ESM 互操作指南

📝 引言

📌 场景/痛点

你一定经历过这种崩溃时刻:

  • 在项目里写了 import express from 'express',运行时报错 SyntaxError: Cannot use import statement outside a module。
  • 或者更离谱的 Default import is not a function,导致你不得不写成 import * as ... 这种丑陋的写法。
  • 看到文件夹里既有 .js 又有 .cjs,还有 .mjs,完全不知道该用哪个。

Node.js 20 已经原生支持 ESM,TypeScript 5.9 也完善了相关的 Node16/NodeNext 解析策略。 是时候彻底告别 require 和复杂的 webpack 别名配置,拥抱原生的模块化标准了。

✨ 最终效果

掌握本文配置后,你的项目将实现"类型"与"运行时"的完美统一。无论是开发环境还是打包产物,都不会再出现模块解析错误。

📖 内容概览

本文将带你理清 ESM 迁移的乱麻:

  1. 核心配置 :package.json 中的 type 字段与 moduleResolution。
  2. 后缀名之谜 :.mts, .cts, .d.mts 到底是什么?
  3. 互操作性:如何在 ESM 项目中正确引入 CommonJS 库。
  4. 动态导入:解决懒加载和循环依赖的利器。

🛠️ 正文

1. 环境准备

请确保 Node.js 版本 >= 20.10.0 (LTS)。

2. 第一步:确立项目性质

在现代 Node.js 项目中,第一步就是在 package.json 中决定你的"底色"。

2.1 开启 ESM 模式 (推荐)

在 package.json 根节点添加:

json 复制代码
{
  "type": "module"
}

这意味着:

  • 默认情况下,.js 后缀的文件被视为 ESM 格式。
  • 你可以自由使用 import/export 语法。
  • 如果你非要用 require,必须把文件后缀改成 .cjs (Common JS)。

3. 第二步:配置 TypeScript (tsconfig.json)

TS 5.9 引入了 Node16 和 NodeNext 模式,专门用于完美对齐 Node.js 的 ESM 行为。

json 复制代码
{
  "compilerOptions": {
    "target": "ES2022", // 现代语法
    "module": "NodeNext", // 🔥 关键:匹配 Node.js 的 ESM 行为
    "moduleResolution": "NodeNext", // 🔥 关键:使用 Node 20 的解析规则
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    
    "esModuleInterop": true
  },
  "include": ["src/**/*"]
}

4. 第三步:理解后缀名的终极奥义

这是最容易晕的地方。请记住这张表:

TS 源文件后缀 编译目标 说明
.ts .js 普通文件,受 package.json 的 type 影响。
.mts .mjs 强制 ESM 格式。无视 package.json 的设置。
.cts .cjs 强制 CommonJS 格式(即使用户开启了 ESM)。
.d.ts - 普通声明文件。
.d.mts - ESM 专用声明文件。
.d.cts - CommonJS 专用声明文件。

2026 年建议:

  • 全源码项目:全部使用 .ts,根目录开 "type": "module"。
  • 发布给他人用的 npm 库:如果是 ESM 专用库,可以使用 .mts 确保用户不能按 CJS 导入,避免 require is not a function 错误。

5. 实战:引入 CommonJS 依赖

2026 年依然有很多老库是 CJS 格式的(如某些老旧的 SDK)。

5.1 错误写法
typescript 复制代码
// 如果库导出了 module.exports = fn
import express from 'express'; 
// 可能会报错:`express` is not a function
5.2 正确写法

在 module: NodeNext 模式下,TS 会严格检查。

  • 如果库提供了 types 字段,TS 会自动处理。
  • 如果没有,或者你只想导入默认导出,推荐使用 命名空间导入 或 动态导入。
typescript 复制代码
// 方案 A:命名空间导入(最稳妥)
import * as express from 'express';
const app = express();

// 方案 B:Dynamic Import (动态导入)
// 适用于懒加载或解决循环依赖
async function init() {
    const { default: express } = await import('express');
    return express();
}

6. 动态导入:顶级 await

在 ESM 模式下(TypeScript 中需开启 module: NodeNext 且 target 较新),你可以直接在文件顶层使用 await。

typescript 复制代码
// src/config.ts
const dbConfig = await import('./db-connection.js'); // 注意:import里要写 .js
// 或者
const secret = await loadSecret();

console.log("Config loaded:", secret);

注意 :TypeScript 的 import 语法中,必须包含文件扩展名 (.js),因为在 ESM 标准中,扩展名是强制要求的,TS 只是把这个路径原样保留在生成的 JS 中。


❓ 常见问题

Q1: 报错 Unknown file extension ".ts" 或 Cannot find module

A:

  1. 检查 tsconfig.json 中 moduleResolution 是否为 NodeNext。
  2. 检查 package.json 是否有 "type": "module"。
  3. 如果是 Node.js 直接运行 TS (如 ts-node),请确保使用支持 ESM 的加载器(如 tsx)。

Q2: 为什么我必须写 import ... from './utils.js' 而不是 .ts?

A:

  • 开发阶段 :开启 allowImportingTsExtensions: true 允许你写 .ts。
  • 运行时 :生成的 JS 里会变成 .js,这是正确的。如果你在源码里写 .js,TS 也能推导回来,但为了 IDE 跳转体验,推荐写 .ts 并开启上述配置。

Q3: 我的项目很老,全是 require,想迁过来怎么办?

A: 别急!

  1. 先把 package.json 的 type 改成 module。
  2. 全局搜索替换 require 为 import(通常用 AST 转换工具,如 import-cjs)。
  3. 遇到报错的地方,通常是默认导出的库,改成 import * as 即可。

🎯 总结

在 Node.js 20+ 和 TS 5.9 的时代,ESM 互操作性已经非常成熟。本文重点掌握了:

  1. 配置底色 :package.json 中 "type": "module" 是开启 ESM 的钥匙。
  2. 解析策略 :使用 module: "NodeNext" 和 moduleResolution: "NodeNext" 对齐 Node 行为。
  3. 后缀名 :理解 .mts (ESM) 和 .cts (CJS) 的强制约束。
  4. 互操作 :使用 import * as 处理老旧的 CommonJS 依赖。

🚀 下期预告:

我们统一了模块系统,也实现了全栈类型共享。但在业务层面,如何用类型系统防止"把金额传成数量"这种低级错误?

下一篇文章我们将深入 《品牌类型与领域驱动设计 (DDD)》,用类型构建业务护城河。

💬 互动环节:

你现在还在用 CommonJS 吗?迁移过程中遇到过什么奇葩报错?

如果觉得文章帮你理清了模块乱麻,请点赞👍、收藏⭐、关注👀,下期更精彩!

相关推荐
guslegend6 分钟前
脚手架原理与本地调试:从 bin 软链接到 npm link
前端·npm·node.js·脚手架·前端工程化
小宋102113 分钟前
A2UI从零实战:Agent安全生成交互界面与事件回传
javascript·人工智能·安全·交互
大包子38 分钟前
structuredClone vs lodash.cloneDeep:核心差异与选型指南
javascript·vue.js
lerhxx1 小时前
AI 应用中的上下文管理 —— 从"上下文窗口"到"分层压缩"的工程实践
前端·javascript
Mahut1 小时前
我在 Mac 上做了个本地剪辑器
javascript·架构·产品
zReadonly2 小时前
不用反复 nvm use 了:nvm-windows 2.x 按项目自动切换 Node.js
前端·node.js
汉堡大王95272 小时前
GPT-6 上线 48 小时,我扒开了 Intelligent UI 的运行机制:DIL、沙箱 Worker 和一个 React 式协调器
前端·javascript·后端
hai_android2 小时前
Chat 聊天模块功能总结
前端·javascript·vue.js
创世虚拟世界2 小时前
我做了一个 3D 虚拟世界基底,基于这个再做 3D 虚拟世界或者元宇宙,会事半功倍
javascript·人工智能·3d·gitee·虚拟现实