ts-node的踩坑之旅

使用ts-node

Typescript 给我们的开发提供了许多便利(类型和名称的自动推断真香,再也不用手打变量名了)。但开发时爽了,运行时却有些麻烦,需要先将 Typescript 代码编译为 Javascript 代码再运行.而很多时候我只是要执行一个一次性的 Nodejs 脚本命令。这时就要使用 ts-node

ts-node 是 Typescript 代码的交互式执行器,如果想要直接执行 Typescript 代码,用 ts-node foo.ts 代替 node foo.ts 即可。

ts 复制代码
const fn = (name : string) => {
    console.log(name);
}

fn("xiaoming")

运行:

ruby 复制代码
$ ts-node foo.ts

xiaoming

但在使用 ts-node 时,常常会遇到一些奇奇怪怪的错误,这些错误主要是由于 Javascript 中的两种模块化规范: Commonjs 与 ESModule 引起的,两者的详细区别可以参考阮一峰老师的《es6标准入门》

踩坑之旅

TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts"

假如你在编写一个 Typescript 库函数,你希望将其编译为 ESModule ,那么你可以通过在 package.json 中声明 "type": "module" 来告诉使用者你的库函数使用的模块规范是 ESModule 。

但如果你使用 ts-node 来运行这个库时,你很可能会遇到一行错误:

css 复制代码
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts"

这是因为通过 tsc --init 生成默认 tsconfig.json 使用的默认模块规范是:"module": "commonjs", 也就是说 Typescript 的默认配置是将代码编译为 commonjs 的模块,而非我们在 package.json 中声明的 module (即 ES module)模块。

ts-node 在运行时会既会读取 package.json 又会读取 tsconfig.json ,而二者的配置相互冲突,于是产生了错误。

那怎么办呢?不用慌, ts-node 的 README.md 文件中已经给出了解决办法:

json 复制代码
{
  "compilerOptions": {
    "module": "ESNext" // or ES2015, ES2020
  },
  "ts-node": {
    // Tell ts-node CLI to install the --loader automatically, explained below
    "esm": true
  }
}

非常好理解,既然 package.jsontsconfig.json 的配置相互冲突,那么让他们一致声明模块为 ESModule 不就好了吗? esm 配置表示在调用 ts-node 时自动添加 ts-node --esm 参数。

因此上面做的事情总结为:

  • 对 Typescript 声明编译目标为 ESModule
  • 对 ts-node 声明运行方法为 ts-node --esm ,将项目看作 ES-Module 模块。

注意

如果你在添加了上述配置后依然报相同的错误,是因为 Nodejs 支持 ES-Module 依然是一个实验性的功能,不稳定, 可见 ECMAScript Modules in Node.js中的一段话:

For the last few years, Node.js has been working to support running ECMAScript modules (ESM). This has been a very difficult feature to support, since the foundation of the Node.js ecosystem is built on a different module system called CommonJS (CJS).

翻译成人话就是:Node.js 底层生态都是用 CommonJS 规范写的,要支持 ESModule 改起来太TM难了。

因此有可能是 Nodejs 的新版本对相关的 API 做了调整, ts-node 还没有来得及进行适配,笔者将 Nodejs 版本从 v20.5.1 降到 v18.17.1 后不再出现错误。

SyntaxError: Cannot use import statement outside a module

既然 ESModule 很容易出问题,那么我们当然得试一试导入导出语句 importexport 了,看看在这种情况下 ts-node 能否正常执行。

下面是一个简单的例子:

json 复制代码
// package.json
{
  ...
  "type": "module",
}
json 复制代码
// tsconfig.json
{
    "compilerOptions": {
        "target": "es2016",
        "module": "esnext",
    }
}
ts 复制代码
// bar.ts
export function bar(){
    console.log('bar');
}
ts 复制代码
// foo.ts
import { bar } from "./bar";

bar()

然后我们执行 ts-node foo.ts

arduino 复制代码
CustomError: Cannot find module 'bar' imported from foo.ts

好的,又双叒叕报错了。因为 ts-node 本质上只是帮我们在内存中做了编译这个步骤,也就是说问题出在 Typescript 编译成 JavaScript 这个环节。 因此我们需要翻阅 Typescript 的文档: 看看 ECMAScript Modules in Node.js 中怎么说,它也举了一个例子,和我们前面的例子非常类似:

ts 复制代码
// ./foo.ts
export function helper() {
    // ...
}
// ./bar.ts
import { helper } from "./foo"; // only works in CJS
helper();

This code works in CommonJS modules, but will fail in ES modules because relative import paths need to use extensions. As a result, it will have to be rewritten to use the extension of the output of foo.ts - so bar.ts will instead have to import from ./foo.js.

上面的代码只能在 CommonJS 模块规范下使用,ESModule 要实现类似的相对路径导入需要使用拓展名,也就是说 ESModule 需要写成下面的样子:

ts 复制代码
// ./bar.ts
import { helper } from "./foo.js"; // works in ESM & CJS
helper();

同理,我们刚才的导入语句需要从:

ts 复制代码
// foo.ts
import { bar } from "./bar";

bar()

修改为:

ts 复制代码
// foo.ts
import { bar } from "./bar.js";

bar()

然后我们尝试执行:

ruby 复制代码
$ ts-node foo.ts

bar

可以正常执行,但很怪,对吧?bar.js 是编译后的产物,我们只有 bar.ts 文件,但写导入语句时却要假装 bar.js 已经被编译好了,已经存在我们的目录下。

为了看看背后发生了什么,我们先不使用 ts-node ,手动编译一下未修改之前的 foo.ts 文件试试:

ts 复制代码
// foo.ts
import { bar } from "./bar";

bar()
复制代码
tsc
js 复制代码
// foo.js

import { bar } from "./bar";
bar();

没有任何更改,根据 ./bar 找不到 ./bar.js 文件,于是我们必须要加上 .js 后缀让其能够匹配到 ./bar.js 文件,即使这个 ./bar.js 文件在编译后才出现.

那么如果我们编译成 CommonJs 模块呢?

json 复制代码
{
  "compilerOptions": {
	  //...
    "module": "commonjs",
  }
}
复制代码
tsc
js 复制代码
//foo.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const bar_1 = require("./bar");
(0, bar_1.bar)();

看起来 requireimport 不同,并不需要我们明确指明 js 文件,它可以通过 ./bar 匹配到 ./bar.js 文件。

关于应用场景

虽然 ESModule 已经被浏览器广泛支持,但由于历史问题,Node.js 主要还是使用 CommandJs 的模块规范,虽然 Typescript 的默认编译配置 CommonJs 能满足大部分场景。但有时候我们依然会遇到需要在 Node.js 中使用 ESModule 的情况。

比如你在开发一个全栈项目,你先将 package.json 中的 type 设置为 module ,表示代码会被编译为 ESModule 让浏览器执行。但同时,你又需要编写服务端的 Node.js 代码,或是需要跑一些 Node.js 脚本,因为他们处于同一项目下,一般来说会遵循相同的模块规范。这时可能就需要用 ts-node 跑这些 ESModole 类型的 Node.js 代码。

相关推荐
清汤饺子2 小时前
OpenClaw 本地部署教程 - 从 0 到 1 跑通你的第一只龙虾
前端·javascript·vibecoding
爱吃的小肥羊4 小时前
比 Claude Code 便宜一半!Codex 国内部署使用教程,三种方法任选一!
前端
IT_陈寒5 小时前
SpringBoot项目启动慢?5个技巧让你的应用秒级响应!
前端·人工智能·后端
树上有只程序猿6 小时前
2026低代码选型指南,主流低代码开发平台排名出炉
前端·后端
橙某人6 小时前
LogicFlow 小地图性能优化:从「实时克隆」到「占位缩略块」!🚀
前端·javascript·vue.js
高端章鱼哥6 小时前
为什么说用OpenClaw对打工人来说“不划算”
前端·后端
大脸怪6 小时前
告别 F12!前端开发者必备:一键管理 localStorage / Cookie / SessionStorage 神器
前端·后端·浏览器
Mr_Mao6 小时前
我受够了混乱的 API 代码,所以我写了个框架
前端·api
小徐_23336 小时前
向日葵 x AI:把远程控制封装成 MCP,让 AI 替我远程控制设备
前端·人工智能
冴羽6 小时前
来自顶级大佬 TypeScript 之父的 7 个启示
前端·typescript