
typescript 默认在 `module: "esnext"` 下移除导入路径中的 `.js` 后缀,导致 node.js esm 环境无法解析模块;需通过 `verbatimmodulesyntax: true`(推荐)或 `allowjs: true` 配合正确配置来保留扩展名。
在 TypeScript 5.0+ 中,allowJs: true 并不能真正解决 .js 后缀缺失问题——它仅允许 TS 编译器读取 .js 文件,但不会影响输出的 import 语句生成逻辑。官方文档也明确指出:allowJs 与模块路径重写无关。真正有效的方案是启用 verbatimModuleSyntax: true(自 TS 4.7 起引入),该选项强制 TypeScript 原样保留源码中 import/export 语句的路径字符串,包括 .js 扩展名。
✅ 正确解决方案:启用 verbatimModuleSyntax
更新你的 tsconfig.json,添加以下关键配置:
{
"compilerOptions": {
"rootDirs": ["src"],
"outDir": "dist",
"lib": ["es2020", "esnext.asynciterable"],
"target": "es2020",
"module": "esnext",
"moduleResolution": "node",
"esModuleInterop": true,
"types": ["node"],
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"verbatimModuleSyntax": true // ? 关键:保留原始 import 路径(含 .js)
}
}✅ 效果:
若你在 User.ts 中显式写为:
import { UserFavoriteRoom } from "./UserFavoriteRoom.js";
import { Room } from "./Room.js";则编译后 User.js 将严格保持:
import { UserFavoriteRoom } from "./UserFavoriteRoom.js";
import { Room } from "./Room.js";⚠️ 注意:verbatimModuleSyntax: true 要求所有 import 路径必须显式带扩展名(.js、.mjs 或 .cjs),且与目标运行时兼容。Node.js ESM 强制要求静态解析路径必须含扩展名(Node.js ESM 规范),因此这是符合标准的最佳实践。
❌ 为什么 allowJs: true 不是正确答案?
- allowJs: true 仅允许 .js 文件参与类型检查和编译流程,不影响 import 语句的 emit 行为;
- 它无法让 TS 在 import "./X.js" 中保留 .js,也不会修复路径截断问题;
- 实际测试表明:即使开启 allowJs,上述 User.ts 中的 .js 仍会在输出中被移除。
? 额外建议:确保项目级一致性
统一使用 .js 扩展名导入(而非 .ts):
TypeScript 源文件在编译后生成 .js,ESM 运行时只加载 .js,因此 import "./X.js" 是语义准确且必需的。启用 resolveJsonModule(如需 JSON):
若有 import data from "./data.json",需同时设 "resolveJsonModule": true 和 "esModuleInterop": true。验证 Node.js 版本支持:
你使用 Node.js 18.16.0 ✅,已完整支持 ESM + .js 扩展名解析(无需 --experimental-specifier-resolution=node)。
? 总结
| 方案 | 是否有效 | 说明 |
|---|---|---|
| verbatimModuleSyntax: true | ✅ 强烈推荐 | 100% 保留源码 import 路径,符合 Node.js ESM 规范 |
| allowJs: true | ❌ 无效 | 与路径生成无关,属误导性答案 |
| 自定义 rollup/webpack 构建 | ⚠️ 过度复杂 | 原生 tsc 即可解决,无需引入构建工具 |
启用 verbatimModuleSyntax 后,重新执行 npm run build,即可生成带 .js 后缀的 import 语句,彻底解决 ERR_MODULE_NOT_FOUND 错误。










