V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
BeautifulSoap
V2EX  ›  Node.js

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

  •  
  •   BeautifulSoap · 1 天前 · 2167 次点击

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

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

    add(1,2)

    我是使用这种写法的,返璞归真,简单明了
    july1995
        12
    july1995  
       1 天前
    写了几天 Python ,我觉得 js 的生态还挺好的。Python 给我的感觉更混乱。
    Donahue
        13
    Donahue  
       23 小时 58 分钟前
    @july1995 明明是 js 更乱
    SingeeKing
        14
    SingeeKing  
    PRO
       23 小时 57 分钟前
    我选择不带扩展名 + 不用 tsc 做编译(只用它做类型检查)
    root71370
        15
    root71370  
       23 小时 34 分钟前 via Android
    别吵了 明明是 java 最乱
    XCFOX
        16
    XCFOX  
       21 小时 51 分钟前
    我选 tsx: https://tsx.is/
    facebook47
        17
    facebook47  
       18 小时 22 分钟前 via Android
    @yooomu 这个现在可用了嘛?出来有些时间了,但是好像没什么浪花🤣🤣🤣
    rick13
        18
    rick13  
       17 小时 19 分钟前
    @shakaraka 现在 bun 生产可用了吗,我看这几天才发 1.3
    mercury233
        19
    mercury233  
       16 小时 8 分钟前
    import { add } from "./utils/calcute";
    这种写法就不应该支持 calcute.* 是文件的情况,只支持 calcute/package.json 就会清晰很多
    subframe75361
        20
    subframe75361  
       15 小时 53 分钟前
    tsup 停止维护了,nodejs 只跑 tsdown 构建的代码,其他情况用 bun
    lqm
        21
    lqm  
       15 小时 37 分钟前
    用 tsx 执行
    fds
        22
    fds  
       14 小时 6 分钟前
    @rick13 #18 我拿来跑脚本还挺稳定的。
    nomagick
        23
    nomagick  
       14 小时 2 分钟前
    恕我直言 node.js 的 esm loader 写了十年还是半成品,基本算是做死了
    就当 node.js 就只能运行纯 commonjs, tsc 的时候永远翻译成 cjs 。
    如果想运行 esm 那就用其他运行时。
    JamesMackerel
        24
    JamesMackerel  
       13 小时 31 分钟前
    所以那个 go 写的 tsc 还有没有消息……
    shakaraka
        25
    shakaraka  
    PRO
       13 小时 14 分钟前
    @rick13 #18 我都在 5 、6 个商业项目上用了 1 、2 年
    opengg
        26
    opengg  
       13 小时 2 分钟前 via Android
    node 很多方面都是狗屎
    craftsmanship
        27
    craftsmanship  
       12 小时 46 分钟前 via Android
    @opengg 愿闻其详
    uni
        28
    uni  
       12 小时 38 分钟前
    5202 年了正确的方法是放弃 node 换 bun
    Ketteiron
        29
    Ketteiron  
       11 小时 50 分钟前   ❤️ 1
    @nomagick #23 很多项目已经逐渐完全放弃 cjs ,也不提供 cjs 产物,全面转向 esm 是必然的事。
    这跟 esm loader 没多大关系,主要是几万个 package 一开始不愿意支持 esm ,毕竟它还能跑对吧。
    有些库作者激进地 esm-only ,用户又要问为什么不支持 cjs ,这十年是用户与作者们在拉扯,nodejs 对此是没什么办法的。
    esbuild 之类的工具尽量解决历史遗留问题,nodejs 没必要重新实现一遍,因为未来某个时间点会放弃 cjs 。
    Terry05
        30
    Terry05  
       11 小时 19 分钟前
    这东西都喷到前端身上,这跟前端有一点关系吗
    molvqingtai
        31
    molvqingtai  
       10 小时 37 分钟前
    我的建议 tsc 检查类型,打包不要用 tsc
    xu33
        32
    xu33  
       9 小时 43 分钟前
    直接用 nextjs 有啥问题没,全 ts
    musi
        33
    musi  
       9 小时 21 分钟前
    我选择用专业的工具进行打包,比如 esbuild/vite
    zogwosh
        34
    zogwosh  
       9 小时 5 分钟前
    nodejs 这种垃圾只配当一个纯粹的 js 运行时使用,
    Zhousiru
        35
    Zhousiru  
       7 小时 47 分钟前
    可以尝试下 extensionless: https://www.npmjs.com/package/extensionless
    pursuer
        36
    pursuer  
       7 小时 18 分钟前
    用 AMD ,想怎么解析你可以自己写,不适合工具链打包场景

    https://github.com/partic2/partic2-iamdee

    https://www.v2ex.com/t/1104713
    mengshouer
        37
    mengshouer  
       7 小时 13 分钟前
    现有项目有什么就用什么
    nomagick
        38
    nomagick  
       6 小时 55 分钟前   ❤️ 1
    @Ketteiron 不你不懂,node.js 的 esm loader 指的是从硬盘网络或文本 buff 加载 js 代码数据并最终转化成 js 对象的过程,其中涉及静态和动态加载,esm 文件中使用 require, 被 require 的文件中使用 import ,被加载的可能是硬盘文件,url 或者代码文本 buff 。 在简单加载之外又涉及到多个切面的插件,专门的加载线程,以及 node.js native binding 的特殊处理。 整个过程比你想像得复杂得多,具体流程一锅粥,代码写得一团乱麻,内部推不动外部看不懂,功能没写完就发布,标记成 Beta/RC ,根本用不了。
    FlashEcho
        39
    FlashEcho  
       5 小时 59 分钟前
    我感觉解法一是不是其实是最常见的,为啥你不用解法一?放弃 tsc 编译使用 bundle 不行吗
    rocmax
        40
    rocmax  
       3 小时 47 分钟前 via Android
    前一阵整理了一下正在开发的 monorepo ,把/packages 下面所有内部模块都改为只用 tsc 输出 d.ts 不编译 js ,/apps 下全部用 bundler 打包。
    streamrx
        41
    streamrx  
       3 小时 34 分钟前 via iPhone
    @rick13 可以
    Ketteiron
        42
    Ketteiron  
       2 小时 32 分钟前
    @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
        43
    v2AKS  
       2 小时 19 分钟前
    esnext 是给 web 项目用的,环境是浏览器,你用 node 执行环境是 Node.js 就要换成 nodenext
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   Solana   ·   1108 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 17:22 · PVG 01:22 · LAX 10:22 · JFK 13:22
    ♥ Do have faith in what you're doing.