在 typescript 项目启用 esm("type": "module" + "module": "esnext")后,.ts 文件导入需显式指定扩展名,但启用 allowimportingtsextensions 会引发额外限制;根本解法是禁用该选项并配合正确的构建与运行时配置。
在 typescript 项目启用 esm("type": "module" + "module": "esnext")后,.ts 文件导入需显式指定扩展名,但启用 allowimportingtsextensions 会引发额外限制;根本解法是禁用该选项并配合正确的构建与运行时配置。
当项目从 CommonJS 迁移至原生 ESM(通过设置 "type": "module" 和 tsc 的 "module": "esnext"),TypeScript 编译器对模块路径的解析行为会发生关键变化:ESM 规范要求导入路径必须为完整、明确的文件路径(含扩展名)或有效包名,不再支持 Node.js CommonJS 时代的“隐式扩展名解析”(如自动尝试 .js、.ts、.d.ts 等)。
因此,以下写法在 ESM 下会失败:
// ❌ 错误:ESM 不允许无扩展名导入 .ts 源文件
import { Button } from "../../../objects/Button";Node.js 的 ESM 加载器无法识别 Button 是指 Button.ts、Button.js 还是 Button.mjs,直接抛出 Cannot find module 错误。
你可能尝试添加 .ts 扩展来“修复”:
// ⚠️ 表面可行但触发新错误
import { Button } from "../../../objects/Button.ts"; // → "allowImportingTsExtensions is required"此时 TypeScript 报错提示需启用 allowImportingTsExtensions —— 但这不是推荐方案。该选项仅用于极少数特殊场景(如 Deno 或自定义构建流程),在标准 Node.js + TypeScript 构建链中启用它会导致:
- 编译输出中残留 .ts 扩展名,破坏运行时兼容性;
- 与 tsc --outDir 或打包工具(Vite、esbuild)路径处理冲突;
- 无法生成有效的 .d.ts 声明文件。
✅ 正确解法:禁用 allowImportingTsExtensions,并统一使用 .js 扩展名进行导入
TypeScript 官方最佳实践明确指出:在 ESM 项目中,应始终导入编译后的 .js(而非源码 .ts)文件。这意味着你的导入语句应指向输出产物,而非源码路径:
-
确保 tsconfig.json 明确禁用危险选项:
{ "compilerOptions": { "module": "esnext", "target": "es2020", "outDir": "./dist", "rootDir": "./src", "allowImportingTsExtensions": false, // ✅ 关键:显式设为 false(默认即 false,但建议显式声明) "moduleResolution": "node16", // 或 "nodenext",启用 ESM-aware 解析 "resolveJsonModule": true, "esModuleInterop": true, "skipLibCheck": true } } -
调整导入路径,指向编译目标(.js):
// ✅ 正确:导入经 tsc 编译后的 .js 文件(假设输出结构与源码一致) import { Button } from "../../../objects/Button.js";? 注意:.js 扩展名是 ESM 运行时必需的,且 tsc 默认生成同名 .js 文件,因此该路径在 dist/ 目录下天然存在。
-
构建与执行工作流建议:
? 重要提醒:
- 不要混用 "type": "module" 与 require() 或 __dirname —— 这些是 CommonJS 特性;
- moduleResolution: "node16" 或 "nodenext" 是 ESM 项目的必备配置,它启用 package.json#exports 和条件导出解析;
- 若项目依赖第三方库,请确认其已提供 ESM 兼容入口(检查 package.json#exports 或 #main/#module 字段)。
遵循以上配置,即可在保持代码简洁性的同时,完全符合 ESM 规范,避免扩展名陷阱与构建异常。









