Node.js 现在能直接跑 TypeScript 了,tsx 和 ts-node 还需要吗?

Node.js 现在能直接跑 TypeScript 了,tsx 和 ts-node 还需要吗?

大前端历险记 / 大鱼

以前写一个脚本,明明只有几十行 TypeScript,也要先纠结装 tsxts-nodets-node-dev,再配一遍 tsconfig.json。现在 Node.js 已经能直接执行一部分 .ts 文件了,这件事对前端项目里的工具脚本很有吸引力。

但它不是"Node 内置了完整 TypeScript 编译器"。这篇我们用几个最小例子把边界跑清楚:哪些脚本可以直接交给 Node,哪些场景还是离不开 tsxts-node

先说结论

如果你只是写项目脚本,比如清理文件、生成配置、调用接口、处理 JSON、跑一些构建前后的自动化任务,Node.js 原生 TypeScript 已经很够用。

如果你的代码依赖下面这些能力,暂时别急着删工具:

  • 需要类型检查;
  • 需要 enum、参数属性这类会生成运行时代码的 TypeScript 语法;
  • 需要 tsx / JSX;
  • 依赖 tsconfig.json 里的 pathsbaseUrljsx 等编译配置;
  • 需要 watch、热重载、开发服务器体验。

一句话:Node 原生 TS 适合跑"接近 JavaScript 的 TypeScript 脚本",不适合替代完整工程编译链路。

准备一个最小 demo

先确认你的 Node 版本。建议使用当前较新的 Node LTS 或 Current 版本,版本太旧会没有这项能力。

bash 复制代码
node -v

建一个干净目录:

bash 复制代码
mkdir node-ts-native-demo
cd node-ts-native-demo
npm init -y
npm pkg set type=module

新建 scripts/hello.ts

ts 复制代码
type User = {
  id: number;
  name: string;
};

const users: User[] = [
  { id: 1, name: "Daisy" },
  { id: 2, name: "Tom" },
];

for (const user of users) {
  console.log(`${user.id}: ${user.name}`);
}

直接跑:

bash 复制代码
node scripts/hello.ts

如果你的 Node 版本支持原生 type stripping,会正常输出:

txt 复制代码
1: Daisy
2: Tom

这里 Node 做的事情很克制:它把 type User = ...: User[] 这些纯类型信息擦掉,然后按 JavaScript 执行剩下的代码。

它不会帮你做类型检查

这是最容易误会的地方。把刚才的代码改坏:

ts 复制代码
type User = {
  id: number;
  name: string;
};

const user: User = {
  id: "1",
  name: "Daisy",
};

console.log(user);

id 明明应该是 number,我们传了字符串。但你直接运行:

bash 复制代码
node scripts/hello.ts

它仍然会执行。

原因很简单:Node 原生能力只负责"擦类型并运行",不负责"检查类型是否正确"。所以项目里仍然应该保留 tsc --noEmit

bash 复制代码
npm install -D typescript
npx tsc --init
npx tsc --noEmit

我会把它们分工看成这样:

txt 复制代码
node xxx.ts      负责快速运行脚本
tsc --noEmit    负责检查类型

不要因为 node script.ts 能跑,就把类型检查从 CI 里删掉。

哪些语法会踩坑

Node 的 type stripping 只能处理"擦掉以后就是合法 JavaScript"的 TypeScript 代码。有些 TS 语法不是单纯类型,它会生成运行时代码,这类就不能直接靠擦除解决。

最典型的是 enum

ts 复制代码
enum Role {
  Admin = "admin",
  User = "user",
}

console.log(Role.Admin);

enum 在 TypeScript 编译后会生成 JavaScript 对象。Node 如果只是擦类型,就没法凭空生成这段运行时代码。

更稳的写法是用普通对象:

ts 复制代码
const Role = {
  Admin: "admin",
  User: "user",
} as const;

type Role = (typeof Role)[keyof typeof Role];

console.log(Role.Admin);

参数属性也是类似问题:

ts 复制代码
class User {
  constructor(public name: string) {}
}

它需要编译成属性赋值。改成普通 JavaScript 能理解的写法:

ts 复制代码
class User {
  name: string;

  constructor(name: string) {
    this.name = name;
  }
}

如果你的项目大量使用这些语法,tsx 或 TypeScript 编译器仍然更省心。

tsconfig 不是它的主场

再看一个前端项目很常见的配置:

json 复制代码
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

然后在脚本里写:

ts 复制代码
import { readConfig } from "@/shared/config";

这在 Vite、Next.js 或 tsx 里可能能跑,但 Node 原生执行 .ts 时不会自动理解你的 paths。它按 Node 自己的模块解析规则走,不会把 @/shared/config 翻译成真实路径。

所以工具脚本我更建议用相对路径,或者直接把脚本组织得简单一点:

ts 复制代码
import { readConfig } from "../src/shared/config.ts";

这里有一个小细节:官方更推荐在 TypeScript 文件里写清楚可运行的扩展名,例如 .ts.mts.cts。这样 Node 执行时不需要猜模块类型。

它适合哪些前端脚本

我会优先把这些场景迁过去:

txt 复制代码
scripts/
  clean.ts
  sync-env.ts
  generate-routes.ts
  check-i18n.ts
  build-report.ts

比如写一个检查环境变量的脚本:

ts 复制代码
const requiredEnvs = ["DATABASE_URL", "OPENAI_API_KEY"];

const missing = requiredEnvs.filter((key) => !process.env[key]);

if (missing.length > 0) {
  console.error(`Missing env: ${missing.join(", ")}`);
  process.exit(1);
}

console.log("All required envs are set.");

放到 scripts/check-env.tspackage.json 里直接这样写:

json 复制代码
{
  "scripts": {
    "check:env": "node scripts/check-env.ts"
  }
}

这类脚本的特点是:逻辑独立、依赖少、启动要快、不需要 JSX、不需要复杂别名。

以前为了跑它装一个运行时转换工具,现在可以少一个依赖。

tsx 和 ts-node 还剩什么价值

tsx 仍然很适合开发体验更重的场景:

  • 要 watch 模式;
  • 要快速跑一个依赖复杂的 TS 入口;
  • 要支持 tsconfig paths
  • 要跑 TSX/JSX;
  • 要在本地开发服务里反复重启。

比如:

bash 复制代码
tsx watch scripts/dev-server.ts

或者:

bash 复制代码
tsx src/playground.tsx

ts-node 更常出现在老项目、测试工具链、REPL、或者一些依赖 TypeScript 编译器行为的环境里。它启动通常没 tsx 轻,但兼容历史生态更久。

我的建议是别做宗教选择,按场景拆:

场景 建议
简单项目脚本 node xxx.ts
类型检查 tsc --noEmit
watch 开发脚本 tsx watch
TSX / JSX tsx 或框架自己的工具链
老项目已有 ts-node 不急着迁
CI 里的脚本任务 能用 Node 原生就用 Node 原生

我会怎么迁移老项目

不要一上来全仓替换。先从 scripts/ 目录挑一个最简单的脚本:

bash 复制代码
node scripts/check-env.ts

能跑通后再做三件事:

bash 复制代码
npx tsc --noEmit
npm run lint
npm run build

如果脚本用了 enum、路径别名、TSX,先别硬改。为了少一个 devDependency 重写一堆业务脚本,通常不划算。

比较稳的迁移标准是:

txt 复制代码
迁移后代码更简单:可以迁
只是为了追新而改写一堆语法:别迁

对前端团队来说,这个能力最大的价值不是"替代 TypeScript",而是让很多边缘脚本不用再挂一层额外运行时。

资源链接

Node.js TypeScript 官方文档:nodejs.org/api/typescr...

Node.js 原生运行 TypeScript 指南:nodejs.org/en/learn/ty...

TypeScript 官方文档:www.typescriptlang.org/docs/

tsx:github.com/privatenumb...

ts-node:typestrong.org/ts-node/

结尾

Node 原生 TypeScript 很适合给项目脚本减负,但它的定位是"擦类型并执行",不是替代完整 TypeScript 工具链。

你们项目里现在有多少 scripts/*.ts 是为了跑一个小任务,额外装了 tsxts-node

相关推荐
阿猫的故乡1 小时前
Vue动态组件+异步组件实战:Tab切换、按需加载、KeepAlive缓存,一次搞定
前端·vue.js·缓存
风骏时光牛马1 小时前
Stylus预处理器完整语法与项目实战详细代码案例
前端
tangdou3690986551 小时前
DevOps Skill工具链:CI/CD流水线搭建全攻略
前端
tangdou3690986551 小时前
前端Skill全家桶:React+Vue+TypeScript开发实战
前端
大大杰哥1 小时前
Vue2学习(3)--组件中的通信方式/组件之间的交互
java·前端·javascript
糖醋丸子1 小时前
D3生成topo 结点连线 webpack 配置兼容ie 11
前端
阿猫的故乡1 小时前
Vue3自定义插件:封装一个全局消息提示插件,所有组件都能直接用
前端·javascript·vue.js
橘子星1 小时前
树与二叉树:从概念到 JavaScript 实现
前端·javascript·面试
小小高不懂写代码1 小时前
Transformer与注意力机制
前端·人工智能