目录

微软为何选择用Go而非Rust重写TypeScript

微软为何选择用Go而非Rust重写TypeScript

最近, TypeScript 宣布用 Go 语言全面重写 TypeScript。重写后的ts在某些测试中实现了 10 倍的速度提升(例如对于VS Code项目),有的甚至高达 15 倍。

https://i-blog.csdnimg.cn/img_convert/5b0960490db56187bf7192678e631094.png

短短几天,其官方库 star数超过了1.4万,各种文章纷至沓来. 但同时大家有一个疑惑,为什么微软选用了Go,而不是最近几年重写万物的Rust?

( )

就此, Michigan TypeScript 频道主持人,也是一位Rust开发者,采访了TypeScript 联合创始人兼首席架构师 Anders Hejlsberg (译者注: 中文一般译为 安德斯·海尔斯伯格 , 丹麦人, 编程语言领域的传奇,Delphi、C#和TypeScript之父). 以下是对 的整理与翻译,过程中为符合中文表达有适当删改.

主持人前言:

JavaScript 从来都不是为了计算密集型的系统级工作负载而设计的,对吧?而 Go 语言却正是为此而生。我们这次追求的是完全的兼容性,真正想要的是让它成为旧编译器即插即用的替代品。但我认为,更值得关注的是这样一个问题:如果有一个类型检查器,它的速度比以前快 10 倍,那么这会带来什么影响?

当我们把这个问题与 AI 以及Agentic Programming等领域的进展结合起来思考时,我们能用 10 倍速度生成的信息做些什么呢?比如,是否可以为 LLM(大型语言模型)提供更多上下文信息?比如,这些类型的实际解析结果是什么?某个符号究竟代表什么?它究竟是在哪里声明的?目前,LLM 只能看到符号的拼写,并不能真正理解它的含义。

如果我们能够以实时的方式赋予 LLM 更高精度的信息,会产生什么样的变化呢?

今天的公告表明,TypeScript 的类型检查器 --- 作为全球最基础的软件开发工具之一 --- 现在的速度提升了 10 倍。这是因为团队已经将代码库迁移到了 Go 语言,这个过程已经持续了数月之久。当 TypeScript 的首席架构师、联合创始人 Anders Hejlsberg 邀请我讨论这一重大变革时,我试着思考了一些问题 --- 这些问题可能是库作者、资深用户、工具开发者以及编译器贡献者所关心的。

我本职工作是写 Rust,我知道很多人都会好奇:为什么他们没有选择 Rust?我一开始也有同样的疑问。但在与 Anders 的对话中,他分享了团队对未来路径的愿景。在听完技术上的考量后,我完全认同 Go 是正确的选择。

不过,我更希望大家关注的不是 TypeScript 迁移到了哪种原生语言,而是类型检查的速度提升了 10 倍,同时内存占用减少了一半,并且实现了并发处理。这意味着 TypeScript 将迎来一个新的时代,带来许多新的可能性。我希望你们能通过 Anders 的分享,深入了解这次变革。

采访正文:

主持人 :大家好,我们今天邀请到 Anders Hejlsberg。他将为我们介绍 TypeScript 领域正在发生的一件大事,并解答一些问题。希望能从库作者的视角、从那些深入使用 TypeScript 的开发者视角,来聊聊这一变化的影响。

Anders,你好!最近怎么样?请向大家介绍一下自己吧!

Anders :我很好,谢谢!我是 Anders Hejlsberg,微软的技术院士(Technical Fellow),目前担任 TypeScript 开源项目的首席架构师。在此之前,我在 C# 语言项目上工作了十多年。而在更早之前,我在 Borland 公司工作,负责 Delphi 和 Turbo Pascal 的开发。所以,我从事编程语言和软件开发工具的工作已经超过 40 年了,甚至更久。

主持人 :哇,真是令人惊叹的职业生涯!其实,不久之前,我还特别喜欢写 C 语言,并且对它的方方面面都非常欣赏,所以要特别感谢你!不过后来,TypeScript 出现了……

也许我们可以从最初的故事讲起。这次新项目的代号是 Corsa ,对吧?而旧代码库的代号是 Strata 。我听说,Strata 其实是 TypeScript 最早的代号,是这样吗?

Anders :是的,没错!大概在 2010 年底到 2011 年初,我们开始着手这个项目。当时,我们在公司内部开发 TypeScript,最初的代号就是 Strata。

主持人 :当时这个项目的主要成员是你和 Luke,对吧?

Anders :是的,当然,还有 Steve Lucco。他编写了最初的原型编译器。当时,他是通过获取 Internet Explorer 的 JavaScript 引擎中的词法分析器(Scanner)和解析器(Parser),然后重新利用这些组件来构建 TypeScript 编译器的原型。那是一套用 C 语言编写的代码库,只是想先做个概念验证(Proof-of-Concept)。Luke 当时是我们的产品经理。所以,一开始是 Steve、我和 Luke 组成了 TypeScript 的核心团队。

主持人 :太棒了!那么快进到最近……你能告诉我们,这次的 Corsa 项目是谁提出来的吗?这一天是什么时候?

Anders :嗯……其实并没有特定的某一天。这种想法在我们脑海里已经存在很久了。你应该也注意到,在 ECMAScript 生态系统中,很多关键工具已经开始向原生代码迁移,比如 、 (译者注: 前者是用Go开发的web打包工具, 后者为用 Rust 编写的Ts/Js编译器) 等等。现在市面上已经有多个原生代码编写的 JavaScript 解析器和 Linter(代码检查工具)。

我们一直在关注这些趋势,并且实际上,在 TypeScript 编译器的构建过程中,我们自己也使用了 esbuild。此外,我们也观察到社区里有多个团队尝试用原生代码重新实现 TypeScript。有些团队从零开始构建,有些则尝试进行迁移,但遗憾的是,这些项目都没能真正形成影响力。

这其实可以理解,因为 TypeScript 是一个非常复杂的项目 --- 目前,我们在 TypeScript 上已经投入了大约 100 人年的开发工作。因此,对于一个个人开发者来说,想要迁移或重写一个高度兼容的 TypeScript 版本,几乎是不可能的任务。这是一项庞大的工程。

我们一直在关注社区中的这些尝试,同时,我们也进行了大量关于性能和可扩展性的讨论。因为这是 TypeScript 用户最常提出的需求之一 --- “我们可以让它扩展得更好吗?可以让它运行得更快吗?”

https://i-blog.csdnimg.cn/img_convert/9d341324b6b67d1714a515c26e92f3c5.jpeg

是的,它确实需要更快。随着软件的发展,代码库只会越来越庞大,而不会变小。项目规模在不断增长,我们的编译器也在不断变大。这也给运行时环境带来了更多压力,比如 V8 引擎和 JavaScript 引擎。由于 JavaScript 采用即时编译(JIT Compilation),随着我们不断增加新功能,TypeScript 的启动时间也在逐渐增加。

我们一直在观察 TypeScript 运行时间的缓慢增长,或者说是逐渐变慢的趋势。因此,我们做了一些性能优化的尝试,并进行了一系列改进。但这些优化通常只能带来 5% 或 10% 的提升,并没有实质性的突破。我们逐渐意识到,我们的优化空间已经接近极限了。

当我们用性能分析工具(Profiler)查看 TypeScript 编译器的运行情况时,我们发现它没有明显的性能瓶颈(Hotspots)。它已经尽可能快地运行了,所有的优化方式都已经被用尽。

因此,在去年 8 月,我们开始思考: 如果我们将 TypeScript 迁移到原生代码,会带来怎样的影响? 我们需要获取一些数据,从而做出更明智的决策,判断是否值得进行这次迁移。

于是,我们开始用不同的语言进行原型开发(Prototyping)。我们尝试了 Rust、Go、C 以及其他一些语言。最终,我们发现 Go 非常符合我们的需求。

在 8 月,我开始将 TypeScript 的词法分析器(Scanner)和解析器(Parser)迁移到 Go,以建立一个基准(Baseline),看看它的性能会有多快,以及从 JavaScript 迁移到 Go 的难度究竟如何。

结果比我们预期的要顺利得多。在短短几个月内,我们就实现了一个可以运行的版本,它能够解析我们所有的源代码,并且不会报错。

从这个阶段开始,我们便能推测出一些性能数据。我们逐渐意识到,这次迁移可以让 TypeScript 的性能提升 10 倍

其中,大约 3 到 3.5 倍 的提升来自于原生代码的执行效率,而另外 3 到 3.5 倍 则来自于并发执行(Concurrency)。两者结合后,我们可以实现 10 倍的性能提升

10 倍的速度提升是一个巨大的突破!一旦你看到这样的可能性,就很难放弃这个方向。与之相比,其他的优化方式都显得微不足道。

主持人 :团队其他成员在得知这个消息时,是什么反应?有没有哪一天,大家突然意识到: “哇,这个方案真的可行!”

Anders :我想,团队内部的反应是兴奋和紧张并存的。

一方面,大家对这个技术方向感到兴奋,因为它带来了前所未有的性能提升。另一方面,这也是一片未知的领域,我们能否真正成功?团队成员能否快速掌握 Go 语言?Go 的工具链是否能像 TypeScript 那样好用?

毕竟,我们已经在 TypeScript 代码库中工作了十多年,突然要迁移到一个全新的语言,这确实带来了很多未知因素。这让我们既充满期待,也有些忐忑。

主持人 :你刚刚提到了 “自举语言”(Self-Hosted Language)的概念。对于不太熟悉这个概念的听众,能否简单介绍一下?

Anders :当然。很多编程语言最初是用其他语言编写的,比如 Go 语言最早是用 C 语言编写的,Rust 最早是用 OCaml 编写的。但当这些语言发展到一定程度后,它们就可以用自己来实现自己的编译器。

TypeScript 也是如此 --- 它是用 TypeScript 自己编写的,这意味着我们一直在用 TypeScript 开发 TypeScript。

但这次迁移相当于 “放弃自举”(Ejecting from Self-Hosting),这在编程语言历史上并不常见。

主持人 :所以,这相当于 TypeScript 从 JavaScript 生态系统中 “脱钩”,改用 Go 作为核心实现?

Anders :是的,在 JavaScript 生态系统中,已经有很多工具从 JavaScript 迁移到原生代码,比如 esbuild、SWC 等等。它们最初都是用 JavaScript 编写的,但后来为了提升性能,迁移到了原生语言。

我们也在认真考虑这个问题。事实上,我们最大的担忧之一,就是放弃自托管是否会带来负面影响。毕竟,TypeScript 一直以来都是用 TypeScript 自己编写的,而这种自托管模式也为我们带来了很大好处。

目前,我们仍然在探索最终的架构方案。可以肯定的是,TypeScript 语言服务(Language Service)的核心部分,特别是语义分析引擎(Semantic Engine),将会是原生代码。但 TypeScript 生态中仍然有很多部分可能会继续用 JavaScript 编写。

编译器提供了所有的信息,但周围仍然有很多东西可能会继续留在 JavaScript 中。我们知道,我们必须在原生部分、Go 以及希望使用其他语言的消费者之间构建一个 API。

这是我们尚未完全解决的问题。但我们确实看到了自托管(self-hosting)的价值。然而,我们也必须现实一点。长期来看,如果我们不考虑其他方案,可能会损失 10 倍的性能提升。所以,我们需要权衡,最终选择哪个方案对社区的利益更大。

主持人: 完全赞同!我希望大家能从这次讨论中得到这个信息。我应该提一下,我的工作主要是使用 Rust,当我听到这个消息时,我非常高兴。对于很多人来说,一个显而易见的问题是: 为什么不是 Rust? 因为社区中的许多工具都在逐渐向 Rust 靠拢。所以,我想直接问你这个问题。我知道你们曾经考察过 Rust,我也想听听为什么最终没有选择 Rust。另外,我也想知道,C# 语言是否曾被考虑过。我的理解是,C# 语言近年来在异步处理、线程池等方面有了很大进步,也许你可以谈谈这两个选择?

Anders: 其中一个关键因素是,我们是在迁移现有代码,而不是从零开始。如果我们是从零开始,那么选择哪种语言可以根据项目需求来决定。例如,如果我们从零开始编写 Rust,我们会从一开始就设计一个不依赖自动垃圾回收(GC)、不过度依赖循环引用的编译器。

但现实是,我们的产品已经有十多年的历史,有数百万的程序员在使用,还有数百万行代码在运行。因此,我们不可避免地会遇到各种兼容性问题。我们的编译器中有很多行为是“随意”决定的 --- 比如在类型推导中,可能有多个候选项都是正确的,而我们的编译器会选择其中一个。这种行为实际上已经成为很多程序依赖的特性。如果新的代码库在这方面的处理方式不同,就可能引发新的错误。

所以,从一开始,我们就知道唯一可行的方案是 迁移现有代码库 。而现有代码库有一些基本假设,其中之一就是 依赖自动垃圾回收 。这个前提基本上就排除了 Rust,因为 Rust 没有自动 GC。

在 Rust 中,你可以使用手动内存管理、引用计数等方式,但 Rust 还有一个额外的限制: 借用检查(Borrow Checker) ,它对数据结构的所有权管理非常严格,尤其是禁止循环数据结构。而我们现有的代码库中,循环数据结构无处不在,比如:

  • AST(抽象语法树) 既有子节点指向父节点,也有父节点指向子节点。
  • 符号表 里的符号可能引用声明,而声明又可能回溯引用符号。
  • 类型系统 也是高度递归的,存在大量循环引用。

如果要适配 Rust,我们就必须重新设计所有这些数据结构,这会让迁移到原生代码的难度变得难以逾越。因此,我们需要一种语言,它既能生成高效的原生代码,又能支持循环数据结构,同时还必须具备自动垃圾回收。

此外,我们还需要 并发支持 ,并且是 共享内存并发 。虽然 JavaScript 通过 Web Workers 提供了并发能力,但它不支持共享内存并发。而我们的编译器需要共享内存并发。

当我们把所有这些需求列出来后,再加上我们希望有优秀的开发工具(比如 VS Code 的支持),最终发现 Go 语言在各方面的表现都非常优秀。因此,我们开始用 Go 进行原型开发,结果发现体验非常好,于是就继续推进了。

主持人: 这确实是一个很现实的考量。我个人对 Rust 充满激情,但我也清楚 Rust 并不是一门“可以在一个周末学会的语言”。Rust 关注的是 尽可能正确 ,即使这会影响开发体验(DX)。

Anders: 对于 JavaScript 开发者来说,Go 的学习曲线显然比 Rust 低得多,这一点我深信不疑。

主持人: 很高兴听到你这么说,因为从 人力资源 的角度来看,这也是一个很重要的决策依据。如果你对比 JavaScript 和 Go,它们的代码结构其实很相似。但如果你对比 JavaScript 和 Rust,尤其是涉及到递归结构时,Rust 的代码就很难让人直接看出它是从 JavaScript 迁移过来的。

这也是 Go 的一个巨大优势。

那么,我们来补充最后一个问题: C#语言呢? C# 是否曾被考虑过?

Anders: 是的,我们确实考虑过 C# 语言。但最终,我们发现 Go 的优势更大。

Go 是我们能选择的 最低级别 的语言,同时仍然具备 自动垃圾回收 。它是最接近原生的语言,同时还提供 GC。相比之下, C#语言 更像是“字节码优先”的语言,虽然某些平台上有 AOT(Ahead-of-Time)编译选项,但它并不适用于所有平台,

从某种程度上来说,C 语言并没有经过十多年的严格打磨,它最初的设计目的也不是为了我们这样的应用。而 Go 在数据结构布局和内联结构体(inline structs)方面更具表现力,这对我们来说是一个很大的优势。

此外,我们的 JavaScript 代码库采用了 高度函数式 的编程风格,我们几乎不使用类(classes),事实上,核心编译器部分根本不使用类。而 Go 也具有类似的特性,它主要由函数和数据结构组成,而不像 C# 那样高度面向对象(OOP)。如果我们选择 C# ,就必须 切换到面向对象的范式 ,这会增加迁移的阻力,而 Go 则是 阻力最小的选择

主持人: 太好了!关于这个问题,我有一些疑问。我过去在 Go 语言的函数式编程方面遇到过很多困难,但听你这么说,似乎你们并没有遇到类似的问题,这也是我想问的一个问题。

Anders: 当我说“函数式编程”时,我的意思是纯粹的函数式风格,即我们主要使用 函数和数据结构 ,而不是对象。我并不是指模式匹配(pattern matching)、高阶类型(higher-kinded types)、单子(monads)之类的概念。我们所谈论的,仍然是一个相对底层的实现方式。

主持人: 我曾深入调试 TypeScript 代码库,虽然我没有大量贡献代码,但在研究编译后的输出时,我发现 TypeScript 代码库 大量使用枚举(enums)和按位运算(bitwise operations) ,尤其是 一元按位运算 来跟踪状态值。然而,Go 似乎没有完全相同的概念,虽然它支持常量(const)分组,看起来有点像枚举,但我很好奇,你们是如何在 Go 里处理这个问题的?

Anders: 你提到的本质上是 TypeScript 拥有比 Go 更丰富的类型系统 ,这一点我完全同意。Go 并没有真正的枚举(enums)概念,尽管它支持常量分组(const grouping)以及 Iota 机制(自动编号),但它仍然有些怪异,类型检查的支持也不如 TypeScript 复杂。

然而,Go 在 位运算(bit manipulation)和标志位(flags)存储 方面的支持却远超 JavaScript。JavaScript 中,所有数据类型本质上都是 浮点数(floating point numbers) ,而在 Go 中,你可以使用各种整数类型,比如 int8int16int32int64 ,既有有符号(signed),也有无符号(unsigned)。相比之下,JavaScript 甚至用 8 字节的浮点数来存储布尔值(true/false),这显然是低效的。

在我们的 JavaScript 编译器中,我们采用了一些优化技巧,比如 在浮点数中打包 31 位信息 ,但这仍然是一个权宜之计。而在 Go 里,我们可以 使用所有的位(bits) ,甚至可以将它们排列成 内联结构体(inline structs) ,并存储在数组中。这种优化使得我们的 内存消耗减少了大约一半

在现代计算机架构中, 内存消耗直接影响速度 。使用更多的内存会导致频繁访问主存,从而降低性能。CPU 处理指令的速度在预测命中时几乎是零周期(zero cycles),但如果发生缓存未命中(cache miss),可能就需要 数千个周期 才能从主存中取回数据。因此, 优化数据结构的布局,减少内存占用,可以显著提升性能

主持人: 听你这么说,我开始思考另一个问题。尽管 Go 的类型系统不如 TypeScript 复杂,但它确实有一些独特的功能。例如,TypeScript 允许 创建不透明类型(opaque types) ,在 Go 里,是否有类似的机制?你每天都在使用 Go,是否有一些 Go 语言的特性让你想要引入到 TypeScript 里?

Anders: 嗯,你的这个问题很有趣,让我思考一下。不过,我可以先提一点,Go 的新版本**引入了“新类型(fresh types)” 的概念,你可以创建一个 int32 的变体,它不同于其他 int32 ,这就是我们在 Go 里实现 类型安全的枚举(enums)**的方法。

至于有没有 Go 的特性值得引入到 TypeScript,我觉得很难说。因为 Go 的类型系统相对简单,它更关注 运行时特性 ,而 TypeScript 主要是 静态类型系统 。不过,在运行时特性方面,我确实希望 JavaScript 也能像 Go 一样拥有 结构体(structs) ,但这是否适合整个 JavaScript 生态系统,我还不太确定。

毕竟, 编译器 本身是一个 极端特殊 的 JavaScript 应用场景。如果有人在十年前告诉我,我会在 JavaScript 里写编译器写十年,我肯定觉得他们疯了。但现实是,我们的团队确实一直在用 JavaScript 开发编译器。

然而,JavaScript 并不是为计算密集型的系统级负载设计的 ,而 Go 恰恰是为这种场景设计的。看看 Kubernetes 这些基于 Go 的大型项目,你就会明白这一点。Go 没有 UI 相关的抽象,它本质上是一个 系统级工具 ,而 TypeScript 编译器也是一个 系统级程序 ,所以 Go 非常适合我们的需求。

主持人: 这确实很有道理。那么,我们来深入探讨一下,这次迁移如何确保整个 TypeScript 生态系统的 平稳过渡 ?我的第一个问题是,TypeScript 没有正式的规范(formal specification) ,它的**参考实现(reference implementation)**就是规范本身。那么,在迁移到新的 Go 代码库时,你们如何确保行为的一致性?

Anders: 这正是我们**选择“迁移”而不是“重写”**的核心原因之一。当你迁移代码时, 最终的语义(semantics)保持不变 。虽然代码的实现方式不同,但输入相同的数据,仍然会得到相同的行为。

主持人: 所以,你的意思是,迁移不会导致任何行为上的变化?

Anders: 是的,我们的目标是 尽可能忠实地保持原有的行为 。我们保留了 所有相同的类型 ,数据结构的布局方式也与 JavaScript 版本一致。当然,在 JavaScript/TypeScript 里,我们大量使用 联合类型(union types)、交叉类型(intersection types) ,以及一些 Go 里没有的高级类型系统特性,因此我们的类型声明方式会有所不同,但核心逻辑仍然保持一致。从语义上讲,我们讨论的仍然是相同的概念。这一点适用于符号(Symbols)、对象模型(Object Model)以及编译器内部的类型系统。

主持人: 太好了!因为库的作者们可能会担心是否需要维护两套类型定义。而听起来,你们正在努力确保这个过渡是平稳的。

Anders: 我们的目标是 99.99% 的兼容性。理想情况下,我们希望对相同的代码基生成完全一致的错误信息。这正是我们一直在努力的方向。

目前,我们开放源码的编译器已经能够无错误地编译和检查整个 Visual Studio Code 代码库,而那可是一个庞大的代码基,包含约 150 万行代码,接近 5000 个源文件,总大小约 50MB。此外,我们也已经非常接近启用所有的测试。

我们知道,我们可以运行 2 万个符合性测试而不会崩溃。我们仍在分析基准数据,并消除一些细微的差异。但我们的目标是完全兼容,确保它能够作为旧编译器的无缝替代品。因为只有这样,我们才能最终摆脱长期维护旧编译器的负担。

主持人: 目前来看,有什么特别困难的挑战会影响这个过渡吗?

Anders: 这是个好问题。如果没有挑战,那当然是最好的答案(笑)。但实际上,确实存在一些复杂的情况。例如,我们在内部对类型的表示方式做了一些调整,尤其是在类型排序方面。

当你有一个类型的联合(Union)时,顺序在某些情况下是重要的,比如当你打印出联合类型时,类型的顺序决定了输出的显示方式。另外,在某些错误消息中,我们需要选择合适的候选项进行错误报告,或者在执行子类型简化时,这些排序都会产生影响。

在旧编译器中,类型排序的方式相对简单但并不确定(非确定性)。它在单线程环境下是确定的,但在多线程环境下可能会有所不同。以前,我们会在创建类型对象时简单地分配一个递增的序列号,这在单线程环境下是可预测的。但在多线程环境下,由于并发的特性,这种方法不再适用。因此,我们需要引入一种确定性的类型排序方式。但这也导致在某些情况下,类型的排序与旧编译器有所不同。虽然理论上联合类型的顺序不应该影响功能,但在某些情况下,它确实会造成变化,我们也需要处理这些问题。

不过,所有这些问题都是可以解决的。我认为目前最大的挑战可能是如何为新的代码库提供一个可版本化(Versionable)且现代化的 API。

在旧的代码库中,源代码本身就是 API 规范。JavaScript 允许你从任何地方调用任何东西,因此编译器的所有内部组件都被暴露成了 API。但在新架构中,我们不能再这样做了。实际上,新代码库目前默认不暴露任何 API,因此我们需要精心设计一个新的 API,并确保它在进程间通信(IPC)环境下仍然高效,而不是简单地通过调用堆栈进行函数调用。

主持人: 我完全理解你的意思。我参与过一些类似的项目,调用一个函数可能有效,但在类型系统中它可能已经被移除(笑)。人们确实会利用这些漏洞。

Anders: 是的(笑)。我们确实在认真考虑 JavaScript API 的设计,并希望它能与其他语言更好地对接。

主持人: 我很高兴这个项目没有用 Rust 编写,因为这意味着它可以更好地与其他语言集成。如果我想用 Zig 编写一个代码生成器(Emitter),这可能会更容易。你们有考虑 WebAssembly(WASM)吗?是否会提供 Rust 或其他语言的绑定(Bindings)?

Anders: 我们的目标是提供语言无关的绑定(Language-Neutral Bindings)。我们确定会支持 语言服务器协议(LSP,Language Server Protocol) ,因为这是我们新的原生语言服务(Native Language Service)的核心架构。而这也是我们一直希望完成的转变。

TypeScript 项目早于 LSP 诞生,事实上,TypeScript 还是 LSP 设计的灵感之一。但我们自己一直没有完全迁移到 LSP,而这次重构给了我们一个机会。LSP 将成为一个通用的 API,所有工具都可以利用它。此外,我们可能还会在 LSP 之上提供额外的功能,以便开发者能够查询更多语言服务器的信息。

不过,LSP 的功能远不及当前 JavaScript API 的丰富程度,因此我们也在研究如何提供一个更丰富、更同步的 API,尤其是如果我们仍然希望部分语言服务继续使用 JavaScript。但目前,我们还没有最终确定 API 的设计方案,我们正在积极探索这个领域。

主持人: 你们计划在过渡到新的 Go 版编译器后,维护 Strata 代码库多久?

Anders: 大概还会维护几年。我们非常谨慎,不希望让任何用户掉队。我们清楚,一些项目可能还没有办法立即迁移到新的原生编译器,因此我们会继续维护旧的 TypeScript 编译器。

不过,到今年年底,我们预计会有一个完全可用的编译器,绝大多数用户都可以使用它。实际上,命令行编译器部分已经非常接近完成。我预计到春末或夏初,它就能投入使用,成为旧编译器的直接替代品。同时,我们正在开发 JSX、JSDoc 支持、项目引用(Project References)、构建模式(Build Mode)和 Watch 模式等功能,这些都在进行中。

目前,我们已经可以用新编译器编译一个单独的项目,它的速度比旧编译器快 10 倍 ,并且能给出相同的错误信息。

主持人: 那意味着 TypeScript-Go 代码库会成为新的主代码库,对吧? (译者注: 目前两者star数为103k

/14k)

Anders: 是的,长期来看,它将成为 TypeScript 的主要代码库。当然,我们仍在探索语言服务的架构,最终可能会有一个原生组件和 JavaScript 组件的结合。但总体来说,新的 Go 版编译器将是 TypeScript 的未来。

主持人: 这个时间表看起来很激进(笑)。但你们的支持周期也很长。我的朋友 Andrist 之前也来过这个频道,他现在有大约 100 个 PR(Pull Requests)在排队。我担心这些 PR 会怎么样?

Anders: 我们会尽量迁移这些 PR。我们在去年 8 月或 9 月选定了一个基准提交(Baseline Commit),作为迁移的起点。因此,我们可以从那个点开始,回溯并挑选合适的 PR 进行迁移。

主持人: 这对 TypeScript 生态系统中的工具开发者来说是个好消息。他们的工具,比如 Linter(代码检查器)、Formatter(格式化工具)等,会因此变得更快吗?

Anders: 这很难一概而论。具体取决于工具的实现方式,以及它们依赖语言服务(Language Services)的程度。有些工具可能只是使用解析器(Parser),而不是完整的语义分析(Semantic Analysis)。不过,我们确实在与生态系统中的主要工具开发者沟通,以帮助他们迁移或优化工具。

主持人: 太好了!你们积极与工具开发者沟通,这对整个社区来说是个好消息。

另外,我想问一下并发模型(Concurrency Model)。你们在引入并发时遇到了哪些挑战?有没有遇到死锁(Deadlock)或其他并发问题?

Anders: 这很有趣,因为我们正在迁移的 TypeScript 编译器,也就是大家过去十年一直在使用的编译器,它本身就是一个 高度函数式(Functional)的代码库

它采用了许多函数式编程的模式,特别是在 不可变性(Immutability) 方面。这种策略可以确保数据的安全共享。例如,在我们扫描、解析并绑定 抽象语法树(AST,Abstract Syntax Tree) 之后,我们基本上会将其视为不可变的。这意味着多个类型检查器可以同时访问相同的 AST,而不会相互干扰。

你可能会问:“但 JavaScript 本身并不支持并发,这有什么影响呢?” 实际上,这一点仍然很重要。因为在开发过程中,你可能会打开多个项目,而这些项目可能都包含相同的文件。我们的做法可以避免创建多个重复的 AST,从而节省大量资源。此外,它还能帮助我们更高效地复用数据。

当你在 语言服务(Language Service) 中编辑代码时,实际上你是在不断地创建新的 程序视图(Program View) 。因为每次编辑文件,整个代码库的状态都会发生变化。然而,大部分代码并没有变化,因此在重建新的程序视图时,我们希望尽可能复用旧的视图数据。这也是为什么 不可变数据结构(Immutable Data Structures) 非常重要的原因。

举个例子,在一个包含 100 个文件的项目里,如果你只修改了其中 1 个文件,每次按下键盘的瞬间,编译器实际上都需要重新构建整个程序视图。但如果我们使用不可变数据结构,那么 99 个未修改的文件的 AST 仍然可以直接复用 ,只需要更新当前编辑的文件即可。

从一开始,我们的编译器就是按照这种方式设计的。可以说,它本质上就是一个 非常适合并发处理的编译器 ,只是一直被限制在一个无法充分利用并发的环境中。而这正是我之前提到的 共享内存并发(Shared Memory Concurrency) 的关键所在。

https://i-blog.csdnimg.cn/img_convert/725466de63e666d699ad57414ca21ff2.jpeg

主持人: 你能具体讲讲并发对你们的帮助吗?JavaScript 不是只能通过 Web Workers 实现并发吗?这方面的限制如何影响你们的开发?

Anders: 是的,JavaScript 的并发模型主要依赖 Web Workers ,但 Web Workers 之间是相互隔离的,无法直接共享内存。它们唯一能共享的是 JSON 数据字节数组(Byte Array) ,但无法共享结构化数据(Structured Data)。

然而,在编译器的某些环节,比如 解析(Parsing) ,我们可以充分利用并发处理能力。解析是一个 高度可并行化(Embarrassingly Parallelizable) 的任务。它的基本流程是:

  1. 读取源文件到内存。
  2. 构建一个数据结构,以便快速解析和导航代码。

在这个过程中,每个源文件的解析都是 完全独立的 。如果你有 5000 个源文件,同时有 8 个 CPU 核心,你可以将这些文件分成 8 份,然后让每个 CPU 负责解析其中的一部分。最终,你会得到 8 组解析后的数据结构。

然而,这个方法只有在所有进程共享相同的内存空间时才有效。而 JavaScript 的 Web Workers 是无法共享内存的,所以如果我们在 JavaScript 里尝试这样做,最终会得到 8 个 孤立的解析结果 ,然后我们还需要跨进程通信,把它们合并起来。而 跨进程通信的开销往往比解析本身还要大 ,最终反而得不偿失。

但在新的架构中,我们可以利用 共享内存 ,让所有的解析进程都在同一个内存空间中运行。这让解析变得 3 到 4 倍 更快。而且,实现并行解析的代码改动非常小,仅仅需要 大约 10 行代码 ,只需要在适当的地方使用 Go 语言的 Goroutines互斥锁(Mutex) 来保护共享资源,比如唯一 ID 生成器(Serial Number Generator)。最终,我们的解析速度显著提升。

主持人: 竟然只需要 10 行代码就能实现这么大的提升?这也太厉害了!

Anders: 是的(笑)。不过,类型检查(Type Checking)的并发优化就 没有那么简单了

与解析不同,类型检查并不是 文件级别的独立任务 。类型检查器的核心理念是 全局视图(Whole Program View) ,即:

  • 代码可以从 其他文件 导入类型,
  • 变量的类型可能依赖于 整个项目的上下文
  • 这意味着 类型检查过程会不断跨越文件边界

举个例子,如果你写了 let x: SomeType ,那么 SomeType 可能定义在另一个文件里。类型检查器必须跳转到那个文件,解析 SomeType 的信息,然后再回到当前文件继续检查。这个过程涉及大量的 跨文件访问 ,而这使得并发处理变得更加复杂。

我们的解决方案是 将整个程序拆分成多个部分 ,然后让 多个类型检查器并行工作 。目前,我们默认把代码库分成 4 个部分 (这个数值可能会调整)。

  • 我们创建 4 个类型检查器 ,每个检查器都能访问 整个程序
  • 但每个检查器 只检查自己负责的那部分文件
  • 这样,它们可以并行进行类型检查,而不需要频繁地跨文件访问。

这样做的好处是:

  1. 并行加速 :类型检查的速度提升了 3 倍
  2. 额外的内存开销较小 :虽然某些类型信息会在不同检查器中重复计算,但整体的内存占用 仍然低于旧编译器
  3. 最终提升 10 倍性能 :新的 Go 编译器本身就比旧的 TypeScript 编译器快 3 倍 ,加上 并发优化再提升 3 倍 ,最终获得 10 倍的整体性能提升

主持人: 这确实是一个巨大的变化!那么,展望未来,你认为这会对 TypeScript 未来 10 年的发展产生什么影响?是否会影响语言特性(Language Features)的设计?

Anders: 我认为这确实是一个重要的转折点。

如果我们在 TypeScript 刚推出 2-3年后 进行类似的重构,可能整个生态系统 还没有准备好 ,甚至我们自己也没有足够的经验去做这样的改变。但现在, TypeScript 和 ECMAScript 都已经成熟 ,并且 JavaScript 语言的发展速度也比过去慢了很多。

比如,从 ES5 到 ES6,我们经历了一次 巨大的变革 ,比如新增了 类(Class)、箭头函数(Lambda)、模块(Module) 等等。但现在,JavaScript 语言的演进节奏明显放缓,而开发者越来越关注 可扩展性(Scalability)和性能(Performance) ,而不是新的类型系统特性。

当然,我们仍然会持续跟进 ECMAScript 规范,也可能会新增一些类型系统特性。但我认为 更重要的事情是:我们如何利用 10 倍更快的类型检查器

  • 结合 AI 和生成式编程(Generative Programming)
  • 提供更 高精度的代码分析
  • 甚至 实时验证 AI 生成的代码的正确性 ,确保它不仅 语法正确 ,而且 语义正确

未来,我们可能会让 AI 代码生成器(如 LLMs) 直接调用 TypeScript 编译器,以 实时检测并校正错误 。这样,我们不仅可以生成代码,还能 让 AI 生成的代码更安全、更可靠 。这无疑是一个令人兴奋的方向!

在实时的方式下,我们不仅可以检查 AI 生成的代码是否在语法上正确,还可以确保其语义上的正确性。如果你打算让 AI 生成代码,并真正交付到生产环境中运行,这是至关重要的。

是的,谁能保证这段代码是安全的呢?让 AI 保持“诚实”的唯一方法就是通过确定性类型检查器或验证器来进行检查。

我认为,这里确实有一些非常有趣的新方向,以前我们根本没有条件去尝试,但现在我们可以开始认真思考这些可能性了。

主持人: 那么,在未来的某个时间点,你认为是否可能会出现一个原生支持 TypeScript 的运行时?我期待这个已经很多年了。当然,我们现在有 Deno,它是用 Rust 编写的,也许这项工作会与它有一些交集。你觉得未来有没有可能基于这个代码库构建一个以 TypeScript 为核心的运行时?

Anders: 在这个行业里,我学会了一件事,那就是永远不要说“不可能”。这确实有可能发生。

不过,我要说的是,JavaScript 运行时当前的部分性能瓶颈,比如 JIT(即时编译)编译,这是 V8 运行时的一部分,但如果采用更原生的编译系统,可能可以绕过这些问题。然而,JavaScript 的对象模型本身也是一个挑战。

例如,JavaScript 允许你随时给对象动态添加新属性(扩展属性),或者计算属性名。实际上,JavaScript 的对象更像是哈希表,而不是像 C 语言那样的结构体,它们在内存中的排列方式完全不同。尽管我们可能会认为它们类似,但它们的行为本质上是不同的。

JavaScript 处理数字的方式也是如此:它没有真正的整数,所有数字都是浮点数。这些特性如果要保持 JavaScript 的语义,就不可能被完全抛弃。

当然,你可以构想一种类似 TypeScript 但具有不同语义的语言,很多人也尝试过这样做,并且可以为其构建一个原生编译器。但问题是:这真的是人们所需要的吗?这很难说。

所以,我不确定未来会如何发展。我曾经希望能够找到某种“魔法粉末”,让 JavaScript 运行时性能提升一个数量级,但老实说,我认为至少在可预见的未来,这种情况不会发生。但谁知道呢?

主持人: 你说的很有道理。我是一个技术爱好者,所以我经常梦想这些事情,不要责怪我(笑)。

最后一个问题。当我看到一些工具迁移到 Rust 时,社区中有时会出现一些担忧,尽管“反对”这个词可能过于强烈。有些人会担心:原本有很多开发者愿意用 TypeScript 和 JavaScript 贡献代码,但当工具迁移到 Rust 后,这些开发者可能会因此流失,或者他们被迫学习 Rust。你可以说,这种情况在 TypeScript 或 Go 语言中也可能发生。

不过,我个人认为这其实是个积极的变化。不同技术社区之间有很多能量,我们已经看到 Rust 生态的成功,Zig 生态(比如 Bun)也有类似的趋势。这说明人们愿意为了自己关心的项目走出舒适区。但我想知道,这对你来说是否是一个考虑因素?你们考虑过第三方贡献者的影响吗?

Anders: 当然,既懂 Go 又懂 JavaScript 的开发者人数肯定比单纯懂 JavaScript 的少。但另一方面,愿意贡献到编译器的开发者数量本就有限,他们通常对底层技术非常感兴趣,并且很多人也有原生开发的经验。

此外,从 JavaScript 迁移到 Go 其实是一个相对温和的过程,Go 并不是一个特别复杂、仪式感很强的语言,而 Rust 就更接近 C++,学习曲线相对陡峭。相比之下,Go 更像是一个现代化的 Python 或 JavaScript。

主持人: 当我写 Go 代码时,我曾在一家公司里做了两年 Go 开发。在一次全员工程会议上,一些工程师抱怨说 Go 语言“平庸”,他们不喜欢 Go 不能做一些“花哨”的事情。然而,我永远不会忘记 CTO 当时的回应。他告诉大家:“你必须理解,Go 语言的‘平庸’是刻意设计的。”

Anders: Go 并不尝试变得复杂,而是追求简单。但它的结果却并不平庸

主持人: 比如 Kubernetes,它绝对不是一个“平庸”的软件项目。Go 语言让我们能够构建出像 Kubernetes 这样庞大且强大的系统,这本身就是一种成功。

**主持人:**太棒了!我对这个项目的下一阶段感到非常兴奋。我认为这是一个很棒的决定,而且你们在做决定时考虑得非常周全,包括第三方贡献、生态兼容性等各个方面。我很高兴看到你们如此认真地对待这个项目。

我一直是这个团队的忠实粉丝,这个项目能够发展到今天,功能如此强大,并且仍在不断壮大,真的令人惊叹。而我们即将迎来一个全新的篇章。祝贺你们!

Anders: 谢谢!我可以告诉你,我们整个团队对这件事都非常兴奋。这对我们来说是一个巨大的动力,我相信这对社区来说也是如此。我认为,这将为 TypeScript 迎来又一个精彩的十年,我们已经做好迎接这段旅程的准备了。

主持人: 感谢你今天的分享!这些信息对于库作者以及 TypeScript 生态的核心开发者来说都非常有价值。

Anders: 我的荣幸,谢谢你们邀请我!

更多参考: