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

2821 次点击
所在节点    Node.js
47 条回复
streamrx
20 小时 53 分钟前
@rick13 可以
Ketteiron
19 小时 51 分钟前
@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
v2AKS
19 小时 38 分钟前
esnext 是给 web 项目用的,环境是浏览器,你用 node 执行环境是 Node.js 就要换成 nodenext
zhennann
9 小时 24 分钟前
vonajs 的模块化体系全量采用 typescript+esm ,支持 node24+,对 dev/build/prod 都做了适配,可以算是目前最佳的实践了: https://github.com/vonajs/vona
Curtion
8 小时 53 分钟前
我一般不使用 tsc 当做编译工具
WarlockMan
6 小时 48 分钟前
入乡随俗,在 nodejs 的环境内就明确给出后缀类型。js 生态中后缀有 .js 和 .mjs 之分,如果提供默认会让情况复杂化。不要过度依赖任何默认行为
wu67
3 小时 9 分钟前
我的选择是, api 项目老实用 js, 前端项目由于有 vite 等社区生态, 倒是可以全 ts, 并且几乎没什么坑.

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

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

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

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

© 2021 V2EX