假设在 ./utils/calcute.ts
中有一个工具函数 add()
export function add(a: number, b: number): number { return a + b; }
然后我们在 main.ts 中需要使用这个 add 函数
tsconfig 配置 module=esnext ,然后假设有如下 main.ts
文件
import { add } from "./utils/calcute"; add(1,2)
使用 tsc 编译后使用 node 运行编译后的 js 文件会报错
node ./dist/main.js ... 省略 code: 'ERR_UNSUPPORTED_DIR_IMPORT', url: 'file:///home/xxxxxx/dist/utils/calcute'
原因是现在的 node 处理 esm 的 import 需要指定具体文件名(即类似 import ./utils/calcute.js
)。不写扩展名的 import 会报错
而 typescript 编译代码对 import 内 from "xxxx"
的部分是不会做任何处理直接保留的。按照 ts 官方的意思就是这部分是模块解析,不应该是 typescript 的工作而应交给 js 运行时(如 node 、浏览器)自己处理,所以 tsc 编译 ts 文件是会完整保留这部分不做任何变动的
基于这种方针,于是就有了两种解法
tsconfig 配置 module=nodenext 和 moduleResolution=nodenext ,然后 main.ts
内容如下
import { add } from "./utils/calcute.js"; // 需要添加 .js 扩展名 add(1,2)
说真的,当年我接触到这种写法的时候是大受震撼的。 在 ts 文件中写 import .js 实在过于丑陋了。我不解、我不适应、我无法接受
但这样的代码经过 tsc 编译后就能正常被 node 执行了,我也只能捏着鼻子用了
本来以为 esm 的问题也就这样了,但没想到到了 2025 年就乱套了
因为 bun, deno 的竞争,不思进取的 node 终于开始迭代起功能了。甚至还破天荒地添加了直接执行 typescript 代码的功能(运行的时候直接丢弃类型信息把 ts 当 js 跑)
这个功能现在在在新 node 中已经默认开启可用了,并且 typescript 也为了这个功能添加多个更新。所以可以预见今后用 node 直接执行 ts 会多起来
然后,这个功能在 esm 上就不出意外得出意外了。还是上面的代码 main.ts
内容如下:
import { add } from "./utils/calcute.js"; // 需要添加 .js 扩展名 add(1,2)
使用 node main.ts
执行后直接报错
node main.ts ... 省略 code: 'ERR_MODULE_NOT_FOUND', url: 'file:///home/xxxxxxxx/utils/calcute.js'
嗯,因为模块的代码位于文件 utils/calcute.ts
中,而 import 语句中写的是 ./utils/calcute.js
,所以 node 理所当然的找不到对应的模块文件报错了
所以为了解决这个问题,tsconfig 后来添加了一个选项 allowImportingTsExtensions ,开启后在 main.ts
中需要将 import 改写成 import .ts 的形式
import { add } from "./utils/calcute.ts"; // 需要 import .ts ,而不是.js add(1,2)
嗯,当年 typescript 的回旋镖就这么砸了回来,现在我们又必须在 ts 文件中写 import .ts 了。并且为了兼容这种写法 typesript 现在还不得不添加新的编译选项 allowImportingTsExtensions
来允许在 ts 文件中 import .ts
但是,这有个问题,启用这个选项必须也启用 noEmit ,也就是说在 typescript 官方那的说法是:我们没有被打脸啊,我们依旧不处理 import 的内容,你想 import .ts 可以,但是你这样写了的话就别用我们的 tsc 来把这种代码编译成 js 了
但问题是实际上开发中,使用 node 直接执行 ts 文件测试,然后在生产环境中使用 tsc 或其他工具编译成 js 运行会很常见
于是如果你想直接 node 执行 ts 代码,那就得放弃将使用 tsc 将代码编译为 js
目前这 esm import 写法已经乱成这样了,大家平时会怎么选?
![]() | 1 irrigate2554 1 天前 ![]() 我选择少用 nodejs 生态 |
![]() | 2 shakaraka PRO 现在经受过的好几个新旧项目全部切换成 bun 了,再次也是选 deno 。业务上基本依赖的第三方包基本全是 esm ,如果没有的话我们会把项目拉下来,用 AI 修改为 esm 的方式,作为一个本地依赖 |
3 craftsmanship 1 天前 via Android ESM 确实是痛点问题 很乱很麻烦 |
4 craftsmanship 1 天前 via Android ![]() 我的建议是 - import 的扩展名为 .ts - tsconfig 里 module 和 moduleResolution 都设为 NodeNext 无需 allowImportingTsExtensions 和 noEmit 且不存在你说的问题 |
5 yooomu 1 天前 遇到了同样的问题,所以换了 deno |
6 SDYY 1 天前 我在 utils/index.ts 中 export 使用 import x from "./utils" |
![]() | 7 Ketteiron 1 天前 tsc 虽然能编译成 js ,但这不是它该干的活,毕竟它只是老老实实地把 ts 翻译成 js 没有任何优化,tsc 用来检查类型就行了。 我的做法是 "moduleResolution": "bundler",后端使用 tsup/tsdown ,前端使用 vite 。 虽然官方推荐显示指定扩展名,但说实话完全没必要,未来真有必要也可以写个脚本全加上。 |
![]() | 8 learnshare 1 天前 看来大家经验都差不多,生态很乱,还频繁遇到这些状况。 统一成 ESM 挺好的,但执行起来不太顺利 |
9 stinkytofux 1 天前 前端真的是乱成了一锅粥了. |
![]() | 10 Ketteiron 1 天前 另外现阶段还是建议用 tsx(不是 react 的那个 tsx) 运行 ts 文件,直到 nodejs 没有这些问题了再说。 |
11 Cbdy 1 天前 import { add } from "./utils/calcute.ts"; add(1,2) 我是使用这种写法的,返璞归真,简单明了 |
![]() | 12 july1995 1 天前 写了几天 Python ,我觉得 js 的生态还挺好的。Python 给我的感觉更混乱。 |
![]() | 14 SingeeKing PRO 我选择不带扩展名 + 不用 tsc 做编译(只用它做类型检查) |
15 root71370 23 小时 26 分钟前 via Android 别吵了 明明是 java 最乱 |
![]() | 16 XCFOX 21 小时 43 分钟前 我选 tsx: https://tsx.is/ |
17 facebook47 18 小时 14 分钟前 via Android @yooomu 这个现在可用了嘛?出来有些时间了,但是好像没什么浪花 |
19 mercury233 16 小时 0 分钟前 import { add } from "./utils/calcute"; 这种写法就不应该支持 calcute.* 是文件的情况,只支持 calcute/package.json 就会清晰很多 |
![]() | 20 subframe75361 15 小时 45 分钟前 tsup 停止维护了,nodejs 只跑 tsdown 构建的代码,其他情况用 bun |
![]() | 21 lqm 15 小时 29 分钟前 用 tsx 执行 |
![]() | 23 nomagick 13 小时 54 分钟前 恕我直言 node.js 的 esm loader 写了十年还是半成品,基本算是做死了 就当 node.js 就只能运行纯 commonjs, tsc 的时候永远翻译成 cjs 。 如果想运行 esm 那就用其他运行时。 |
24 JamesMackerel 13 小时 23 分钟前 所以那个 go 写的 tsc 还有没有消息…… |
![]() | 26 opengg 12 小时 54 分钟前 via Android node 很多方面都是狗屎 |
27 craftsmanship 12 小时 38 分钟前 via Android @opengg 愿闻其详 |
28 uni 12 小时 30 分钟前 5202 年了正确的方法是放弃 node 换 bun |
![]() | 29 Ketteiron 11 小时 42 分钟前 ![]() @nomagick #23 很多项目已经逐渐完全放弃 cjs ,也不提供 cjs 产物,全面转向 esm 是必然的事。 这跟 esm loader 没多大关系,主要是几万个 package 一开始不愿意支持 esm ,毕竟它还能跑对吧。 有些库作者激进地 esm-only ,用户又要问为什么不支持 cjs ,这十年是用户与作者们在拉扯,nodejs 对此是没什么办法的。 esbuild 之类的工具尽量解决历史遗留问题,nodejs 没必要重新实现一遍,因为未来某个时间点会放弃 cjs 。 |
![]() | 30 Terry05 11 小时 11 分钟前 这东西都喷到前端身上,这跟前端有一点关系吗 |
![]() | 31 molvqingtai 10 小时 29 分钟前 我的建议 tsc 检查类型,打包不要用 tsc |
![]() | 32 xu33 9 小时 35 分钟前 直接用 nextjs 有啥问题没,全 ts |
![]() | 33 musi 9 小时 13 分钟前 我选择用专业的工具进行打包,比如 esbuild/vite |
34 zogwosh 8 小时 57 分钟前 nodejs 这种垃圾只配当一个纯粹的 js 运行时使用, |
![]() | 35 Zhousiru 7 小时 39 分钟前 可以尝试下 extensionless: https://www.npmjs.com/package/extensionless |
![]() | 36 pursuer 7 小时 10 分钟前 |
![]() | 37 mengshouer 7 小时 5 分钟前 现有项目有什么就用什么 |
![]() | 38 nomagick 6 小时 47 分钟前 ![]() @Ketteiron 不你不懂,node.js 的 esm loader 指的是从硬盘网络或文本 buff 加载 js 代码数据并最终转化成 js 对象的过程,其中涉及静态和动态加载,esm 文件中使用 require, 被 require 的文件中使用 import ,被加载的可能是硬盘文件,url 或者代码文本 buff 。 在简单加载之外又涉及到多个切面的插件,专门的加载线程,以及 node.js native binding 的特殊处理。 整个过程比你想像得复杂得多,具体流程一锅粥,代码写得一团乱麻,内部推不动外部看不懂,功能没写完就发布,标记成 Beta/RC ,根本用不了。 |
39 FlashEcho 5 小时 51 分钟前 我感觉解法一是不是其实是最常见的,为啥你不用解法一?放弃 tsc 编译使用 bundle 不行吗 |
40 rocmax 3 小时 38 分钟前 via Android 前一阵整理了一下正在开发的 monorepo ,把/ackages 下面所有内部模块都改为只用 tsc 输出 d.ts 不编译 js ,/apps 下全部用 bundler 打包。 |
![]() | 42 Ketteiron 2 小时 24 分钟前 @nomagick #38 稍微研究了一下,确实很蛋疼 https://github.com/nodejs/node/issues/52697 https://github.com/nodejs/node/issues/55782 更蛋疼的是目前依旧需要为 cjs 浪费大量人力,而 cjs loader 看起来越来越好 https://github.com/nodejs/node/issues/52219 esm loader 确实需要重构(顺便重构成以 C++ 为主),但阅读了相关 issue 几乎可以下结论除非不再支持生态中的 cjs ,不然重构无法开头,有点理解你说的永远翻译成 cjs https://github.com/nodejs/node/issues/50356 如果不准备放弃对 cjs 的支持,esm loader 理论上无法进行有效重构,且 nodejs 团队其中的一部分人员在 v24 依旧认为 cjs 不应该被放弃 https://github.com/nodejs/node/pull/57460 顺便还发现了 typescript 也放弃了 esm-only https://github.com/microsoft/TypeScript/pull/58419 |
![]() | 43 v2AKS 2 小时 11 分钟前 esnext 是给 web 项目用的,环境是浏览器,你用 node 执行环境是 Node.js 就要换成 nodenext |