2025 年 node 项目,乱成一锅粥的 typescript ESM import 写法该怎么选?

1 天前
 BeautifulSoap

假设在 ./utils/calcute.ts 中有一个工具函数 add()

export function add(a: number, b: number): number {
  return a + b;
}

然后我们在 main.ts 中需要使用这个 add 函数

写法 1, import 不带扩展名:

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 文件是会完整保留这部分不做任何变动的

基于这种方针,于是就有了两种解法

  1. 放弃 tsc 编译使用 bundle
  2. 下面的写法 2

写法 2:import .js

tsconfig 配置 module=nodenext 和 moduleResolution=nodenext ,然后 main.ts 内容如下

import { add } from "./utils/calcute.js"; // 需要添加 .js 扩展名

add(1,2)

说真的,当年我接触到这种写法的时候是大受震撼的。 在 ts 文件中写 import .js 实在过于丑陋了。我不解、我不适应、我无法接受

但这样的代码经过 tsc 编译后就能正常被 node 执行了,我也只能捏着鼻子用了

本来以为 esm 的问题也就这样了,但没想到到了 2025 年就乱套了

写法 3: import .ts

因为 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 写法已经乱成这样了,大家平时会怎么选?

2320 次点击
所在节点    Node.js
43 条回复
irrigate2554
1 天前
我选择少用 nodejs 生态
shakaraka
1 天前
现在经受过的好几个新旧项目全部切换成 bun 了,再次也是选 deno 。业务上基本依赖的第三方包基本全是 esm ,如果没有的话我们会把项目拉下来,用 AI 修改为 esm 的方式,作为一个本地依赖
craftsmanship
1 天前
ESM 确实是痛点问题 很乱很麻烦
craftsmanship
1 天前
我的建议是
- import 的扩展名为 .ts
- tsconfig 里 module 和 moduleResolution 都设为 NodeNext
无需 allowImportingTsExtensions 和 noEmit 且不存在你说的问题
yooomu
1 天前
遇到了同样的问题,所以换了 deno
SDYY
1 天前
我在 utils/index.ts 中 export
使用 import x from "./utils"
Ketteiron
1 天前
tsc 虽然能编译成 js ,但这不是它该干的活,毕竟它只是老老实实地把 ts 翻译成 js 没有任何优化,tsc 用来检查类型就行了。
我的做法是 "moduleResolution": "bundler",后端使用 tsup/tsdown ,前端使用 vite 。
虽然官方推荐显示指定扩展名,但说实话完全没必要,未来真有必要也可以写个脚本全加上。
learnshare
1 天前
看来大家经验都差不多,生态很乱,还频繁遇到这些状况。
统一成 ESM 挺好的,但执行起来不太顺利
stinkytofux
1 天前
前端真的是乱成了一锅粥了.
Ketteiron
1 天前
另外现阶段还是建议用 tsx(不是 react 的那个 tsx) 运行 ts 文件,直到 nodejs 没有这些问题了再说。
Cbdy
1 天前
import { add } from "./utils/calcute.ts";

add(1,2)

我是使用这种写法的,返璞归真,简单明了
july1995
1 天前
写了几天 Python ,我觉得 js 的生态还挺好的。Python 给我的感觉更混乱。
Donahue
1 天前
@july1995 明明是 js 更乱
SingeeKing
1 天前
我选择不带扩展名 + 不用 tsc 做编译(只用它做类型检查)
root71370
1 天前
别吵了 明明是 java 最乱
XCFOX
1 天前
我选 tsx: https://tsx.is/
facebook47
1 天前
@yooomu 这个现在可用了嘛?出来有些时间了,但是好像没什么浪花🤣🤣🤣
rick13
1 天前
@shakaraka 现在 bun 生产可用了吗,我看这几天才发 1.3
mercury233
23 小时 49 分钟前
import { add } from "./utils/calcute";
这种写法就不应该支持 calcute.* 是文件的情况,只支持 calcute/package.json 就会清晰很多
subframe75361
23 小时 34 分钟前
tsup 停止维护了,nodejs 只跑 tsdown 构建的代码,其他情况用 bun

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1166656

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX