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):把 : stringinterface、类型参数这类内容移除,让剩下的部分变成合法 JavaScript,再交给引擎执行。

从实现路径上看,主流 runtime 大致可以归为两类。

0) 内置高性能转译器(Bun / Deno)

它们把转译能力直接塞进 runtime 的二进制里,做到“开箱即跑”。

  1. Bun:核心思路是“快”。它会把 TS 里的类型相关语法快速剥离,然后让 JavaScriptCore 执行结果。
  2. 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 都全量检查,开发体验会瞬间倒退回“先编译再运行”的时代。

于是现代工程形成了一种事实上已经成为共识的策略:动静分离

  1. 静态(开发时):交给 IDE(比如 VS Code 的 TS Server)和 CI 里的 tsc --noEmit,把类型当成“可提前发现的问题”。
  2. 动态(运行时):runtime 专注执行,把 TypeScript 当成“更好写的 JavaScript”,默认只做剥离不做校验。

换句话说:

运行时负责“把车开起来”,类型系统负责“让你别开错路”。


2) 深度对比:谁才更像“全家桶”

把“能不能跑 TS”拆开后,你会发现真正的差异在于:runtime 除了执行,还想不想把工程化能力一起打包提供。

维度DenoBunNode.js
底层实现Rust + V8Zig + JavaScriptCoreC++ + V8
默认执行 TS✅(内置处理)✅(内置处理)✅(实验性)
类型校验入口✅(例如 deno check❌(通常交给外部)❌(通常交给外部)
设计哲学开箱即用、内置配套极致速度、尽量少负担生态最大、稳健演进

0) 为什么 Deno 看起来“更重”

因为 Deno 的目标不是只做“跑起来”,而是尽量把开发者常用的能力内置进去:检查、格式化、测试、权限模型、模块管理等。

这种路线的好处是:在一个全新环境里,你不需要先装一堆依赖、再拼装工具链,就能获得相对完整的工程体验;代价是 runtime 本体更“大”,并且它对“官方推荐的工作方式”有更强的主张。

1) 为什么 Bun 看起来“更像赛车”

因为 Bun 的叙事核心就是速度。它尽可能把“会拖慢执行”的事情外置,把“能让 run 更快”的事情内置。

所以如果你追求的是“脚本能秒起、服务吞吐更高、开发反馈更快”,Bun 的路线会非常有吸引力;但如果你期待 runtime 顺手帮你兜住类型正确性,那通常需要你额外引入静态检查流程。

2) Node.js 的优势从来不在“全家桶”

Node.js 更像基础设施的“底盘”:生态巨大、边界清晰、组合自由。

它在 TS 直跑这件事上变得更积极,并不代表它会走向“一体化大而全”,而更像是在补齐一个长期被社区工具(如 tsxts-node、打包器 dev server)覆盖的使用场景。


3) 开发者怎么选:你要“裸奔”,还是要“安全感”

把它们的 TS 支持看透后,选择其实变得更简单:你到底更看重什么。

  1. 追求极致速度与简单:倾向 Bun。适合工具脚本、对吞吐敏感的服务、希望减少“等待”的开发流程。
  2. 追求严谨与工程完备:倾向 Deno。适合希望开箱即用、减少依赖拼装、偏好一套工具统一体验的团队或个人。
  3. 保守派与庞大生态:继续 Node.js。适合生产环境的稳定性诉求、以及需要深度依赖现有 npm 生态的项目;TS 直跑可以作为补充,但类型检查仍建议交给 tsc --noEmit 和 CI。

4) 结语:所谓“原生支持”,其实是“校验”与“执行”的解耦

TypeScript 的“原生支持”并不是让 JavaScript 引擎突然学会了类型系统,而是现代 runtime 通过剥离类型,把 TypeScript 当成一种更好写的输入格式;同时把“类型正确性”从运行时移出,交给 IDE 与 CI 做静态保障。

一句话总结就是:

它们都在“假装读懂 TS”,以换取更快的启动与更好的即时反馈;至于写得对不对,多半是你和 IDE 之间的秘密。

你更倾向于“裸奔”的速度,还是“校验”的安全感?