假设在 ./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 23 小时 16 分钟前 ![]() 我选择少用 nodejs 生态
|
![]() |
2
shakaraka PRO 现在经受过的好几个新旧项目全部切换成 bun 了,再次也是选 deno 。业务上基本依赖的第三方包基本全是 esm ,如果没有的话我们会把项目拉下来,用 AI 修改为 esm 的方式,作为一个本地依赖
|
3
craftsmanship 21 小时 13 分钟前 via Android
ESM 确实是痛点问题 很乱很麻烦
|
4
craftsmanship 21 小时 6 分钟前 via Android ![]() 我的建议是
- import 的扩展名为 .ts - tsconfig 里 module 和 moduleResolution 都设为 NodeNext 无需 allowImportingTsExtensions 和 noEmit 且不存在你说的问题 |
5
yooomu 21 小时 5 分钟前
遇到了同样的问题,所以换了 deno
|
6
SDYY 20 小时 31 分钟前
我在 utils/index.ts 中 export
使用 import x from "./utils" |
![]() |
7
Ketteiron 20 小时 9 分钟前
tsc 虽然能编译成 js ,但这不是它该干的活,毕竟它只是老老实实地把 ts 翻译成 js 没有任何优化,tsc 用来检查类型就行了。
我的做法是 "moduleResolution": "bundler",后端使用 tsup/tsdown ,前端使用 vite 。 虽然官方推荐显示指定扩展名,但说实话完全没必要,未来真有必要也可以写个脚本全加上。 |
![]() |
8
learnshare 20 小时 1 分钟前
看来大家经验都差不多,生态很乱,还频繁遇到这些状况。
统一成 ESM 挺好的,但执行起来不太顺利 |
9
stinkytofux 19 小时 58 分钟前
前端真的是乱成了一锅粥了.
|
![]() |
10
Ketteiron 19 小时 41 分钟前
另外现阶段还是建议用 tsx(不是 react 的那个 tsx) 运行 ts 文件,直到 nodejs 没有这些问题了再说。
|
11
Cbdy 19 小时 8 分钟前
import { add } from "./utils/calcute.ts";
add(1,2) 我是使用这种写法的,返璞归真,简单明了 |
![]() |
12
july1995 17 小时 58 分钟前
写了几天 Python ,我觉得 js 的生态还挺好的。Python 给我的感觉更混乱。
|
![]() |
14
SingeeKing PRO 我选择不带扩展名 + 不用 tsc 做编译(只用它做类型检查)
|
15
root71370 17 小时 22 分钟前 via Android
别吵了 明明是 java 最乱
|
![]() |
16
XCFOX 15 小时 39 分钟前
我选 tsx: https://tsx.is/
|
17
facebook47 12 小时 10 分钟前 via Android
@yooomu 这个现在可用了嘛?出来有些时间了,但是好像没什么浪花🤣🤣🤣
|
19
mercury233 9 小时 56 分钟前
import { add } from "./utils/calcute";
这种写法就不应该支持 calcute.* 是文件的情况,只支持 calcute/package.json 就会清晰很多 |
![]() |
20
subframe75361 9 小时 41 分钟前
tsup 停止维护了,nodejs 只跑 tsdown 构建的代码,其他情况用 bun
|
![]() |
21
lqm 9 小时 25 分钟前
用 tsx 执行
|
![]() |
23
nomagick 7 小时 50 分钟前
恕我直言 node.js 的 esm loader 写了十年还是半成品,基本算是做死了
就当 node.js 就只能运行纯 commonjs, tsc 的时候永远翻译成 cjs 。 如果想运行 esm 那就用其他运行时。 |
24
JamesMackerel 7 小时 19 分钟前
所以那个 go 写的 tsc 还有没有消息……
|
![]() |
26
opengg 6 小时 50 分钟前 via Android
node 很多方面都是狗屎
|
27
craftsmanship 6 小时 34 分钟前 via Android
@opengg 愿闻其详
|
28
uni 6 小时 26 分钟前
5202 年了正确的方法是放弃 node 换 bun
|
![]() |
29
Ketteiron 5 小时 38 分钟前
@nomagick #23 很多项目已经逐渐完全放弃 cjs ,也不提供 cjs 产物,全面转向 esm 是必然的事。
这跟 esm loader 没多大关系,主要是几万个 package 一开始不愿意支持 esm ,毕竟它还能跑对吧。 有些库作者激进地 esm-only ,用户又要问为什么不支持 cjs ,这十年是用户与作者们在拉扯,nodejs 对此是没什么办法的。 esbuild 之类的工具尽量解决历史遗留问题,nodejs 没必要重新实现一遍,因为未来某个时间点会放弃 cjs 。 |
![]() |
30
Terry05 5 小时 7 分钟前
这东西都喷到前端身上,这跟前端有一点关系吗
|
![]() |
31
molvqingtai 4 小时 25 分钟前
我的建议 tsc 检查类型,打包不要用 tsc
|
![]() |
32
xu33 3 小时 30 分钟前
直接用 nextjs 有啥问题没,全 ts
|
![]() |
33
musi 3 小时 9 分钟前
我选择用专业的工具进行打包,比如 esbuild/vite
|
34
zogwosh 2 小时 53 分钟前
nodejs 这种垃圾只配当一个纯粹的 js 运行时使用,
|
![]() |
35
Zhousiru 1 小时 35 分钟前
可以尝试下 extensionless: https://www.npmjs.com/package/extensionless
|
![]() |
36
pursuer 1 小时 6 分钟前
用 AMD ,想怎么解析你可以自己写,不适合工具链打包场景
https://github.com/partic2/partic2-iamdee https://www.v2ex.com/t/1104713 |
![]() |
37
mengshouer 1 小时 1 分钟前
现有项目有什么就用什么
|
![]() |
38
nomagick 43 分钟前
@Ketteiron 不你不懂,node.js 的 esm loader 指的是从硬盘网络或文本 buff 加载 js 代码数据并最终转化成 js 对象的过程,其中涉及静态和动态加载,esm 文件中使用 require, 被 require 的文件中使用 import ,被加载的可能是硬盘文件,url 或者代码文本 buff 。 在简单加载之外又涉及到多个切面的插件,专门的加载线程,以及 node.js native binding 的特殊处理。 整个过程比你想像得复杂得多,具体流程一锅粥,代码写得一团乱麻,内部推不动外部看不懂,功能没写完就发布,标记成 Beta/RC ,根本用不了。
|