
最近一个关于 Node.js 的消息可能已经传到了你的耳边:从 Node.js 22.6 版本开始,开发者可以直接运行 .ts 文件了。
这意味着不再需要 tsc 编译器,也不需要 ts-node 这类工具,只需在命令行中执行这样一行命令:
node --experimental-strip-types your-file.ts
这一切背后的核心技术,叫做类型剥离。听起来很酷,但它究竟是怎么运作的?在实际使用中有哪些限制?它是否会彻底改变我们编写 TypeScript 的方式呢?这篇文章将为你一一解答。
什么是类型剥离?
TypeScript 从本质上说是 JavaScript 的超集——所有合法的 JavaScript 代码也都是合法的 TypeScript 代码。
那么反过来想一下:如果你把一段 TypeScript 代码中所有的类型信息都删除掉,剩下的不就是纯粹的 JavaScript 吗?这正是类型剥离的核心思想所在。
传统的做法是使用 tsc 编译器将 TypeScript 代码转换成 JavaScript,这个过程涉及语法解析、类型检查、目标代码生成等多个步骤。而类型剥离则更为直接和简单粗暴:它直接将代码中的类型注解替换成等长的空白字符,然后交给 JavaScript 引擎去执行。这与 Java 中的“类型擦除”概念有些相似,但更为彻底——连独立的编译步骤都省去了。
其实,Deno 和 Bun 这两个运行时早已支持这项能力,但真正让“类型剥离”进入主流开发者视野的,还是 Node.js。
在 Node.js 中实际体验
让我们来看一个具体的例子:
// animal.ts
interface Animal {
name: string;
winged: boolean;
}
function move(creature: Animal): string {
if (creature.winged) {
return `${creature.name} takes flight.`;
}
return `${creature.name} walks the path.`;
}
const bat: Animal = {
name: 'Bat',
winged: true,
};
console.log(move(bat));
如果尝试直接用 node animal.ts 运行,会得到一个语法错误:
SyntaxError: Unexpected identifier ‘Animal’
因为原生的 Node.js 引擎无法识别 interface 这样的 TypeScript 语法。但是,如果我们加上类型剥离的实验性标志:
node --experimental-strip-types animal.ts
程序便能成功运行,并输出:
Bat takes flight.
剥离后的代码长什么样?
需要明确的是,类型剥离并非简单地“删掉”类型,而是将它们替换成等长的空白字符。这样设计有一个非常关键的好处:保持源代码的行号不变。
经过剥离处理后的 animal.ts 文件,其内容大致会变成这样:
// 接口声明被整体替换成空白
function move(creature) {
// 参数后的 ‘: Animal’ 和函数返回值 ‘: string’ 被移除
if (creature.winged) {
return `${creature.name} takes flight.`;
}
return `${creature.name} walks the path.`;
}
const bat = {
// 变量后的 ‘: Animal’ 被移除
name: 'Bat',
winged: true,
};
console.log(move(bat));
看起来有些奇怪,但这正是其设计的巧妙之处。由于只是用空白填充,文件的行数和每行代码的起始位置都得以保留。
再见了,Source Map
用空白替换类型的做法,带来了一个巨大的额外优势:你可能不再需要 source map 了。

如果你使用 TypeScript 有一段时间,对 source map 多半是又爱又恨。它的作用是将运行时的 JavaScript 代码位置映射回原始的 TypeScript 文件,以便在调试或报错时能定位到正确的行号。然而,source map 的稳定性时常令人头疼:映射时常中断、变量名对应错误、堆栈跟踪的行号对不上等问题屡见不鲜。
而类型剥离直接从根本上解决了这个问题——你在 IDE 中看到的第 10 行代码,就是运行时实际执行的第 10 行。断点能够精确命中,堆栈跟踪也清晰无误。更重要的是,你的构建流程中因此少了一个需要管理和分发的“产物”。
这也印证了一个核心观点:类型信息本质上是开发时的辅助工具,运行时并不需要它们的存在。我们在编写代码时有类型系统保驾护航,写完之后将类型替换成空白,程序逻辑依然能够正确执行。
代价是什么?
任何技术方案都有其代价,类型剥离也不例外。有些 TypeScript 特性无法被简单地“替换成空白”,因为它们需要编译器生成额外的运行时代码才能工作。这些特性包括:
- 枚举
- 命名空间
- 类参数属性
import = 语法
如果你在代码中使用了上述特性,开启类型剥离将会导致运行时错误。例如:
// ❌ 错误:枚举声明在剥离后无法运行
enum Direction {
Up,
Down,
Left,
Right,
}
class Point {
// ❌ 错误:参数属性需要编译器生成 `this.x = x` 的代码
constructor(
public x: number,
public y: number,
) {}
}
参数属性的问题尤其典型:public x: number 这种语法糖依赖于编译器在构造函数中注入 this.x = x 这样的赋值语句,单纯的“空白替换”无法实现这一逻辑。为此,TypeScript 5.8 版本专门引入了 erasableSyntaxOnly 这一编译选项,帮助开发者在编码阶段就识别出这些不兼容的语法。
Zod:类型剥离的运行时搭档
类型信息在运行时被移除了,那么我们该如何在运行时校验数据的结构呢?例如,你需要验证一个 API 返回的对象是否符合 Animal 接口的定义,但剥离之后这个接口在运行时已不复存在。
这正是 Zod 这类运行时验证库大显身手的地方。Zod 的 Schema 是用普通的 JavaScript 对象和函数定义的,即使经过类型剥离也会完整保留,从而提供 TypeScript 静态类型无法给予的运行时校验能力。
同时,Zod 也是传统 TS 枚举的优秀替代品。在类型剥离环境中被禁用的 TS 枚举,可以用 Zod 枚举来替代。Zod 枚举是实实在在的 JavaScript 对象,在运行时存在,并且同样能通过 z.infer 导出对应的静态类型。

来看一个结合使用的例子:
import { z } from 'zod';
// 1. 定义 Zod schema(该对象在运行时完整保留)
const AnimalSchema = z.object({
name: z.string(),
winged: z.boolean(),
});
// 2. 从 schema 推断 TypeScript 类型(这行代码在运行时会被剥离)
type Animal = z.infer<typeof AnimalSchema>;
这里 type Animal = z.infer<...> 这行类型声明会被类型剥离器移除,而 AnimalSchema 这个运行时验证对象则被完整保留。这是一种非常优雅的设计模式:将 TypeScript 的类型检查能力浓缩为一行代码,在编译时与运行时之间搭建了一座稳固的桥梁。
这对 JavaScript 生态意味着什么?
类型剥离的意义远不止于 Node.js 的一个实验性标志。实际上,TC39(JavaScript 标准委员会)有一个名为“类型注解”的提案,其目标正是让 JavaScript 原生支持类型语法。
其思路与类型剥离高度相似:运行时引擎会完全忽略类型语法,而开发工具(如 IDE、Linter)则可以利用这些语法进行静态类型检查。这个提案目前尚处于 Stage 1 阶段,但类型剥离在 Node.js、Deno 等主流运行时中的普及,很可能会加速它的进程。
该提案的愿景甚至超越了 TypeScript,明确提到了对 Flow 和 Closure Compiler 等类型的兼容性。未来我们或许能看到一种统一的“JavaScript 类型语法”标准。在《JavaScript 现状》年度调查中,“静态类型”一直是开发者呼声最高的“缺失功能”。这一天,或许真的正在向我们走来。
浏览器端暂时还不行
目前面临的一个现实问题是:类型剥离主要在后端运行时(Node.js、Deno、Bun)中可用。浏览器环境尚未原生支持。Chrome 和 Safari 遇到 : string 这样的类型注解时,会直接抛出语法错误。
这意味着当前的开发体验出现了一种分裂:
- 后端开发:可以享受“无构建”流程带来的便捷。
- 前端开发:仍然需要依赖 Vite、Webpack 等构建工具进行编译转换。
这也正是 TC39 “类型注解”提案如此重要的原因——只有当浏览器也原生支持忽略类型语法时,前后端的开发体验才能得到真正的统一。
总结与展望
类型剥离让一个事实变得愈发清晰:类型本质上是编码阶段的辅助工具,不应成为运行时的负担。
过去十年,我们已经默认企业级的 JavaScript/TypeScript 项目必须配备复杂的构建流程。而 --experimental-strip-types 标志以及 TC39 的相关提案,正在挑战这一固有认知。它们共同指向一个更简洁的未来:编写完代码,直接运行。类型信息在编辑器中默默守护着你,并在代码执行时悄然退场。
当然,距离这个理想愿景的完全实现还有一段路要走。枚举、参数属性等语法限制需要开发者调整编码习惯来规避,浏览器端的原生支持仍需等待。但技术演进的方向已经非常明确。
如果你还没有尝试过,不妨将 Node.js 升级到 22.6 或更高版本,亲自体验一下无需编译、直接运行 TypeScript 文件的感觉:
node --experimental-strip-types your-file.ts
这种简洁直接的开发体验,确实值得一试。随着技术的不断演进,未来我们或许能在 云栈社区 看到更多关于这一主题的深度讨论和实践案例。