Alex
Deno, Bun, Node.js 原生支持 TypeScript 的“谎言”与真相
- javascript
- typescript
- deno
- bun
- nodejs
- runtime
作为开发者,你一定感受到 JS Runtime 这两年卷得离谱:Deno 从诞生之初就高举“原生支持 TypeScript”,Bun 紧随其后主打极致性能,就连“老大哥” Node.js 也开始提供实验性的 .ts 直跑能力。
但这里有一个常被忽略的问题:
V8 明明读不懂 .ts,它们到底是怎么“直接运行 TypeScript”的?
答案会让很多人失望,但也会让你对现代工程体系更清醒:它们并没有在运行时做类型检查,甚至大多数时候只是“把类型擦掉”而已。
0) 核心真相:天下武功,唯“擦”不破
先打破一个幻觉:目前没有任何一个主流 JavaScript 引擎(V8 / JavaScriptCore)能直接理解 TypeScript 语义。
所谓“原生支持 TS”,本质都是在运行前或运行中做了一次 隐形转译(transpilation)——更准确地说是 类型剥离(strip types):把 : string、interface、类型参数这类内容移除,让剩下的部分变成合法 JavaScript,再交给引擎执行。
从实现路径上看,主流 runtime 大致可以归为两类。
0) 内置高性能转译器(Bun / Deno)
它们把转译能力直接塞进 runtime 的二进制里,做到“开箱即跑”。
- Bun:核心思路是“快”。它会把 TS 里的类型相关语法快速剥离,然后让 JavaScriptCore 执行结果。
- Deno:同样走“内置转译”的路线(常见实现基于 Rust 生态的转译能力),并且围绕工程体验做了更完整的配套能力。
你可以把它们理解为:runtime 本体里自带了一个“超快的 TS→JS 处理器”,让你在命令行里感觉像是“引擎直接执行 TS”。
1) 实验性的类型剥离(Node.js 22.6+)
Node.js 近年的实验路线更克制:通过 --experimental-strip-types 提供“能跑就行”的类型剥离能力。
它不承诺覆盖 TypeScript 的全部语法形态,而是聚焦于“把常见的类型标注去掉”。因此当你使用一些需要真实语义转换的 TS 特性时(例如某些 enum / namespace 相关写法或其他非纯类型层语法),它可能会直接报错拒绝执行。
这也解释了一个现象:你会觉得 Node 的这条路径更像“把 TS 当成带类型注释的 JS”,而不是“完整支持 TS 语言”。
1) 消失的类型检查:为什么它们默认都不会报错
既然能跑 .ts,那如果我写错了类型,它们会不会在运行时告诉我?
结论:默认情况下,它们通常不会。
原因不复杂:运行时要的是“启动快、反馈快”。而 TypeScript 官方的类型检查(tsc)在大项目里可能需要秒级甚至十几秒级的计算——如果每次 run 都全量检查,开发体验会瞬间倒退回“先编译再运行”的时代。
于是现代工程形成了一种事实上已经成为共识的策略:动静分离。
- 静态(开发时):交给 IDE(比如 VS Code 的 TS Server)和 CI 里的
tsc --noEmit,把类型当成“可提前发现的问题”。 - 动态(运行时):runtime 专注执行,把 TypeScript 当成“更好写的 JavaScript”,默认只做剥离不做校验。
换句话说:
运行时负责“把车开起来”,类型系统负责“让你别开错路”。
2) 深度对比:谁才更像“全家桶”
把“能不能跑 TS”拆开后,你会发现真正的差异在于:runtime 除了执行,还想不想把工程化能力一起打包提供。
| 维度 | Deno | Bun | Node.js |
|---|---|---|---|
| 底层实现 | Rust + V8 | Zig + JavaScriptCore | C++ + V8 |
| 默认执行 TS | ✅(内置处理) | ✅(内置处理) | ✅(实验性) |
| 类型校验入口 | ✅(例如 deno check) | ❌(通常交给外部) | ❌(通常交给外部) |
| 设计哲学 | 开箱即用、内置配套 | 极致速度、尽量少负担 | 生态最大、稳健演进 |
0) 为什么 Deno 看起来“更重”
因为 Deno 的目标不是只做“跑起来”,而是尽量把开发者常用的能力内置进去:检查、格式化、测试、权限模型、模块管理等。
这种路线的好处是:在一个全新环境里,你不需要先装一堆依赖、再拼装工具链,就能获得相对完整的工程体验;代价是 runtime 本体更“大”,并且它对“官方推荐的工作方式”有更强的主张。
1) 为什么 Bun 看起来“更像赛车”
因为 Bun 的叙事核心就是速度。它尽可能把“会拖慢执行”的事情外置,把“能让 run 更快”的事情内置。
所以如果你追求的是“脚本能秒起、服务吞吐更高、开发反馈更快”,Bun 的路线会非常有吸引力;但如果你期待 runtime 顺手帮你兜住类型正确性,那通常需要你额外引入静态检查流程。
2) Node.js 的优势从来不在“全家桶”
Node.js 更像基础设施的“底盘”:生态巨大、边界清晰、组合自由。
它在 TS 直跑这件事上变得更积极,并不代表它会走向“一体化大而全”,而更像是在补齐一个长期被社区工具(如 tsx、ts-node、打包器 dev server)覆盖的使用场景。
3) 开发者怎么选:你要“裸奔”,还是要“安全感”
把它们的 TS 支持看透后,选择其实变得更简单:你到底更看重什么。
- 追求极致速度与简单:倾向 Bun。适合工具脚本、对吞吐敏感的服务、希望减少“等待”的开发流程。
- 追求严谨与工程完备:倾向 Deno。适合希望开箱即用、减少依赖拼装、偏好一套工具统一体验的团队或个人。
- 保守派与庞大生态:继续 Node.js。适合生产环境的稳定性诉求、以及需要深度依赖现有 npm 生态的项目;TS 直跑可以作为补充,但类型检查仍建议交给
tsc --noEmit和 CI。
4) 结语:所谓“原生支持”,其实是“校验”与“执行”的解耦
TypeScript 的“原生支持”并不是让 JavaScript 引擎突然学会了类型系统,而是现代 runtime 通过剥离类型,把 TypeScript 当成一种更好写的输入格式;同时把“类型正确性”从运行时移出,交给 IDE 与 CI 做静态保障。
一句话总结就是:
它们都在“假装读懂 TS”,以换取更快的启动与更好的即时反馈;至于写得对不对,多半是你和 IDE 之间的秘密。
你更倾向于“裸奔”的速度,还是“校验”的安全感?