编程 Node.js 原生跑 .ts:类型剥离不是转译,Enum 直接报错,类型还得靠 tsc

2026-09-09 00:04:41

Node.js 原生跑 .ts:类型剥离不是转译,Enum 直接报错,类型还得靠 tsc

以前跑一个 .ts 文件,得先选路线:tsc 先编译、ts-node 带类型检查、或者 tsx 这种 esbuild 封装。Node 从 v23.6(以及 v22.18 LTS)开始,默认就能直接 node app.ts 执行,不用先编译,也不用装运行时。背后机制是类型剥离(type stripping),和传统转译有本质区别,坑都埋在这。

类型剥离做了什么

Node 遇到 .ts 文件时,内部 loader(Amaro,基于 Rust 的 swc)把可擦除的 TypeScript 语法替换成空白,然后执行剩下的 JavaScript。

// 剥离前
const port: number = 3000;
// 剥离后
const port = 3000;

冒号和类型注解被删掉,但字符位置保留——这一行前后长度一致,堆栈里的行列号仍指向原文件,不需要 source map。这也是它和转译最关键的不同:没有产物文件、没有重排,只是就地挖掉类型。

哪些语法会直接报错

类型剥离只处理“删掉后不影响运行语义”的语法:类型注解、接口、类型别名、import type 这类都行。

需要生成 JS 代码的语法它不处理,默认直接报错:

  • enum(会生成反向映射对象)
  • 构造函数的参数属性(constructor(private x: number)
  • 带运行时代码的 namespace
  • 装饰器(decorator)

想跑这些,要么退回老工具,要么在 Node 里开转译模式 --experimental-transform-types(需生成 source map,性能开销大)。Node 26 起该 flag 已移除。.tsx 文件不支持——JSX 变换不在类型剥离范围内,是独立问题。

最大的坑:不做类型检查

node app.tstsc app.ts。Node 只删类型,从不验证。给 number 变量赋 string,运行时照样执行,因为它看到的代码里已经没有类型这回事了。类型剥离替你免掉的是构建/转译这一步,不是校验这一步。类型安全得自己补:

  • 运行时:node app.ts 直接跑
  • 类型检查:npx tsc --noEmit 单独跑,CI 里必挂一道

tsconfig 怎么配

Node 运行时不读 tsconfig(忽略 paths、旧语法降级),但 tsconfig 决定 tsc 和编辑器会不会顺着 Node 行为走。官方推荐 TS 5.8+ 配这份:

{
  "noEmit": true,
  "target": "esnext",
  "module": "nodenext",
  "rewriteRelativeImportExtensions": true,
  "erasableSyntaxOnly": true,
  "verbatimModuleSyntax": true
}
  • erasableSyntaxOnly:让 tsc 在源码出现无法剥离的语法(enum、参数属性等)时报错——能通过 tsc 的代码,就一定能被 Node 直接跑。
  • verbatimModuleSyntax:强制类型导入显式带 type,配合剥离语义,避免 import 被当值导入、运行时抛错。
  • rewriteRelativeImportExtensions:编译时把 .ts 后缀改写掉。编辑器与 tsc 检查 import 路径带 .tsallowImportingTsExtensions

只跑脚本不产出 JS 就用 noEmit: true;要分发 .js 就别开。

三个容易翻车的细节

  1. import 必须带 .ts 扩展名。ESM 下和 JS 一样强制全扩展名:import './file.ts',不是 import './file'require('./file.ts') 同理。老 ts-node 项目迁移常在这挂。
  2. 模块系统不自动转换。跑 ESM,就在最近父级 package.json"type": "module"import/export;要 CommonJS 用 require/module.exports.mts 恒为 ESM,.cts 恒为 CommonJS。
  3. 本地 importtype 会运行时炸。类型剥离靠 type 关键字识别“这只是类型导入”。不带 type 会被当值导入,最终运行时报错。verbatimModuleSyntax 强制对齐。

适合什么、什么时候别用

类型剥离最顺手:构建脚本、CLI、内部工具、快速原型、后端 service 开发循环。node --watch server.ts 连 nodemon 都省,零依赖零配置:

{
  "start": "node server.ts",
  "dev": "node --watch server.ts",
  "typecheck": "tsc --noEmit"
}

typecheck 单独占脚本位——它必须在 CI 里跑,运行时不兜底。

要发 npm 包给浏览器端、或重度依赖 enum/装饰器等需运行时转换特性,就别指望剥离:浏览器产物和 npm 包分发仍需真实构建。这类场景用 tsx 拿全量支持,npx tsx file.tsnode --import=tsx file.ts

参考

复制全文 生成海报 TypeScript 原生支持 Node.js 类型剥离

推荐文章

程序员茄子在线接单