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

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

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

add(1,2)

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

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

https://ex.noerr.eu.org/t/1166656

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

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

© 2021 V2EX