找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2606

积分

0

好友

370

主题
发表于 8 小时前 | 查看: 2| 回复: 0

TypeScript类型剥离概念图

最近一个关于 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 了

禁用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 导出对应的静态类型。

Zod与Type Schema结合示意图

来看一个结合使用的例子:

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

这种简洁直接的开发体验,确实值得一试。随着技术的不断演进,未来我们或许能在 云栈社区 看到更多关于这一主题的深度讨论和实践案例。




上一篇:Go开发者2025年调查报告:语言生态稳健,AI工具质量成最大槽点
下一篇:Java程序员心声:当产品经理说“需求很简单”的隐藏代价
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-27 18:19 , Processed in 0.348524 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表