TypeScript 7.0 源码级深度拆解:微软为什么用 Go 重写编译器,10× 提速背后的并发架构与工程取舍
2026 年 7 月,微软正式发布 TypeScript 7.0——自 2012 年诞生以来最激进的一次底层重构:编译器从「用 TypeScript 写 TypeScript」的自举实现,彻底迁移到 Go。完整构建通常提速 8~12 倍,编辑器响应快到几乎感觉不到延迟。这篇文章不复述发布通稿,而是从编译器工程的第一性原理出发,拆解这次重写到底改了什么、为什么是 Go、共享内存并行怎么落地,以及作为一线开发者你该怎么迁移、会踩哪些坑。
一、背景:一个「用自己写自己」的编译器,是怎么被自己拖垮的
要理解 TypeScript 7.0 的意义,得先理解它之前十几年背着的一个包袱——自举(bootstrapping)。
TypeScript 从第一天起,编译器 tsc 本身就是用 TypeScript 写的,编译成 JavaScript,跑在 Node.js 上。这在语言设计上是一件很浪漫的事:它向世界证明「TypeScript 足够成熟,连自己都能用它写出来」。但浪漫是有代价的,而且这个代价随着生态规模指数级放大。
问题的核心不在 TypeScript 语言本身,而在它的宿主运行时:
单线程事件循环。 JavaScript 的并发模型是事件循环 + 单线程执行。Node.js 有 Worker Threads,但线程之间默认不共享堆内存,传递对象要经过结构化克隆(structured clone)或序列化。对一个需要在整棵 AST、符号表、类型关系图上反复穿梭的类型检查器来说,这几乎等于「无法真正并行」。你有 16 个核,编译器只能用 1 个。
JIT 预热与 GC 抖动。 V8 的 JIT 很强,但它是为「长时间运行的服务」优化的。
tsc是典型的短命进程:跑一次构建,退出。等 JIT 把热点函数编译成优化机器码,进程可能都快结束了。加上类型检查会瞬间创建海量小对象(每个类型节点、每个符号、每个关系缓存项),V8 的 GC 在这种分配模式下频繁触发 Stop-The-World。对象模型的内存开销。 JS 对象是动态的、带隐藏类(hidden class)的哈希表结构。一个类型节点在内存里远比它「应该」占的字节数大。当你的 monorepo 有几百万行 TS、几十万个类型时,光是把符号表塞进内存就能吃掉好几个 G。
结果就是大家熟悉的日常痛苦:VS Code 里改一行代码,IntelliSense 转半天圈;大型项目 tsc --noEmit 一次要跑一两分钟;CI 里类型检查经常是最慢的那一环。
微软内部有个被反复引用的基准:用旧编译器加载 VS Code 自己的代码库(约 150 万行 TS),编辑器要等 9.6 秒才能响应。 这不是某个烂项目的问题,这是 TypeScript 团队自己的旗舰产品。当你的工具链的性能上限,成了整个生态的效率天花板时,重写就不再是「要不要」,而是「什么时候」。
TypeScript 团队给出的答案是:把编译器从 TypeScript 移植到一门原生编译、支持真并行的语言。 这个内部代号叫 Corsa 的项目,就是后来的 typescript-go,最终成为 TypeScript 7.0。
二、灵魂拷问:为什么是 Go,而不是 Rust / C# / Zig?
这是这次重写里争议最大、也最有嚼头的决策。TypeScript 的作者、编译器领域的传奇人物 Anders Hejlsberg(同时也是 C#、Delphi、Turbo Pascal 的主设计师)亲自解释过选型逻辑。这里我用工程视角把它拆开讲。
2.1 最硬的约束:这是一次「移植」,不是「重写」
先明确一个关键前提——TypeScript 团队做的是 port(移植),不是 rewrite(重写)。
区别巨大。重写意味着重新设计架构、重新做算法决策,然后你会得到一个行为和旧版微妙不一致的新编译器,社区要花好几年去趟平各种边界 case。而移植的目标是:新代码在结构上尽可能逐行对应旧代码,保证语义 100% 一致,直接复用十年积累的测试套件。
这个约束一旦确立,选型的天平就彻底倾斜了。旧编译器是用 TypeScript 写的,它大量依赖:
- 可空的、自动垃圾回收的对象图(节点互相引用,还有大量循环引用)
- 函数式的、闭包满天飞的编码风格
- 结构化的、鸭子类型的数据流
你要找一门语言,让「把这些 TS 代码几乎一比一翻译过去」成为可能。
2.2 逐个淘汰候选者
Rust 被淘汰的核心原因:所有权模型和 AST 的循环引用天生冲突。
编译器的 AST 和符号表是典型的图结构:一个节点指向父节点,父节点又指向一堆子节点;一个符号被无数个引用位置指向。这种「多所有者 + 循环引用」的数据结构,恰恰是 Rust 借用检查器最难受的场景。你要么到处套 Rc<RefCell<T>> 把编译期检查退化成运行时检查(还牺牲性能),要么用 arena + 索引(Vec<Node> + NodeId)彻底改写数据结构——那就不是移植了,是重写。Hejlsberg 说得很直白:用 Rust 的话,代码结构会和原来差太远,移植的等价性根本无法保证。
C# 被淘汰的原因更微妙——毕竟这是 Hejlsberg 自己的语言。 C# 有 GC、有很好的多线程、语法也和 TS 接近。但两个问题:一是 C# 长期以来对「无运行时依赖的原生单文件可执行程序」支持不够顺滑(AOT 是后来才成熟的),而编译器工具要求分发一个小巧、启动快、跨平台的原生二进制;二是 C# 的对象默认在堆上、引用语义为主,而编译器对内存布局的控制欲很强,需要大量值类型(struct)来压缩内存、提升缓存命中。用 C# 能做,但要处处逆着语言习惯走。
Zig / C++ 被淘汰:没有 GC,或 GC 得自己造。 编译器里对象生命周期极其复杂(一个类型可能被缓存、被复用、被跨阶段引用),手动内存管理会把移植变成排雷。团队明确表示不想在移植过程中额外背上内存管理的心智负担。
2.3 Go 胜出的四个理由
Go 恰好落在所有约束的交集里:
有 GC,但可控。 Go 的垃圾回收让「TS 里满天飞的对象引用」可以近乎一比一翻译,不用重新设计所有权。同时 Go 的 GC 是低延迟并发式的,配合值类型能把压力压到很低。
结构体(struct)+ 指针给了内存布局控制权。 这是 Go 相对 C#/JS 的关键优势:你可以用
struct把数据紧凑排布,用指针精确控制共享,用切片(slice)做连续内存的批量存储。类型节点、符号这些高频对象可以设计成缓存友好的布局,直接砍掉旧版 JS 对象模型的内存膨胀。goroutine + channel 是「真并行」,且写起来轻。 这是提速的核心引擎。Go 的 goroutine 由运行时调度到多个 OS 线程上,共享同一个地址空间的堆内存——这意味着多个 goroutine 可以并发读同一棵 AST、同一张符号表,不需要拷贝、不需要序列化。这正是 Node.js Worker Threads 做不到的。
原生编译 + 跨平台单文件二进制 + 快启动。
go build直接产出各平台的静态二进制,没有 JIT 预热,进程一起来就是原生速度。对tsc这种短命进程,这是决定性的——省掉了 V8 冷启动和 JIT 预热的全部开销。
一句话总结选型:Go 是唯一一门能让「逐行移植一个满是循环引用对象图的 TS 编译器」和「真正利用多核共享内存并行」这两个目标同时成立的语言。 它不是最快的(Rust 极限性能更高),也不是最优雅的,但它是约束满足意义上的最优解。
三、核心概念:忠实移植策略与新编译管线
3.1 「Faithful Port」到底是什么意思
TypeScript 7.0 的工程哲学可以浓缩成一个词:faithful(忠实)。团队没有借重写的机会去「优化算法」或「重构架构」,而是刻意保持和旧版逐行对应。
为什么这么克制?因为编译器的正确性是刚需。TypeScript 的类型系统有海量的边界行为——各种条件类型、映射类型、infer、协变逆变、控制流收窄……这些行为经过十年打磨,社区代码里写死了对它们的依赖。任何一个细微的语义漂移都可能让成千上万个项目编译报错。
所以移植的验收标准非常硬核:新旧编译器跑同一套测试套件,输出的诊断信息(错误码、错误位置、错误文本)必须完全一致。 十年积累的测试用例就是那把标尺。这也是为什么 7.0 敢宣称「行为和 6.0 严格一致」——它不是靠嘴保证的,是靠让两个实现通过同一批测试保证的。
3.2 经典的编译管线,被搬进了 Go
TypeScript 的编译流程本身没变,还是那套经典四段式,只是每一段都用 Go 重新实现:
源码 (.ts)
│
▼
┌─────────────┐
│ Scanner │ 词法分析:字符流 → Token 流
│ (扫描器) │
└─────────────┘
│
▼
┌─────────────┐
│ Parser │ 语法分析:Token 流 → AST(抽象语法树)
│ (解析器) │
└─────────────┘
│
▼
┌─────────────┐
│ Binder │ 绑定:遍历 AST,建立符号表(Symbol Table)与作用域
│ (绑定器) │
└─────────────┘
│
▼
┌─────────────┐
│ Checker │ 类型检查:核心中的核心,最耗时的阶段
│ (检查器) │
└─────────────┘
│
├──▶ 诊断信息 (Diagnostics)
└──▶ Emitter → 产出 .js / .d.ts
四个阶段里,Checker(类型检查器)是绝对的性能黑洞。旧版里它是一个上万行的单体函数集合,充满了递归、缓存和惰性求值。它耗掉的时间通常占整个编译的 70% 以上。所以并行化的战场,主要就在这里。
3.3 数据结构层面的降本
在 Go 里重新实现这套管线,最直接的收益来自数据结构。举个例子,一个 AST 节点在旧版 JS 里大概长这样(概念示意):
// 旧版 (JS 对象,动态属性,隐藏类)
interface Node {
kind: SyntaxKind;
pos: number;
end: number;
parent?: Node;
flags: NodeFlags;
// ……几十个可选属性,很多时候是 undefined
// 每个对象都带 V8 的对象头、隐藏类指针
}
在 Go 里,可以用紧凑的 struct + 切片,把节点批量分配在连续内存里:
// 新版 (Go struct,可控内存布局)
type Node struct {
Kind SyntaxKind
Pos int32 // 用 int32 而非 int64,省一半
End int32
Flags NodeFlags
Parent *Node // 指针,精确共享
Data nodeData // 接口/联合,按需承载子类信息
}
// 节点可以批量存在一个大切片里,缓存友好
type NodePool struct {
nodes []Node // 连续内存,遍历时 CPU 缓存命中率高
}
int32 替代 number(JS 里 number 恒为 64 位浮点)、连续内存布局、去掉 V8 对象头——这些微观优化叠加在几百万个节点上,就是几个 G 内存和大量缓存未命中的差别。性能不是靠一个魔法开关,是靠在最高频的对象上抠出来的。
四、架构核心:共享内存并行是怎么落地的
这是整篇文章最硬的部分。10× 提速里,「原生编译」贡献一部分(大约 2~3 倍,来自去 JIT、去 GC 抖动、紧凑内存),而剩下的倍数主要来自并行。
4.1 为什么 JS 版本没法并行,Go 版本可以
回到最根本的差异:内存共享模型。
Node.js 的 Worker Threads 之间不共享普通堆对象。要把一棵 AST 从主线程传给 worker,你得序列化再反序列化(或用 SharedArrayBuffer 手动铺二进制,但那意味着放弃对象模型)。类型检查器需要频繁随机访问整棵 AST 和全局符号表,这种「拷贝一份给每个 worker」的开销,比并行省下的时间还多。所以旧版 tsc 实际上是纯单线程的。
Go 完全不同。所有 goroutine 跑在同一个进程的同一个堆上,共享地址空间。一个 goroutine 持有的 *Node 指针,另一个 goroutine 直接就能读。只要你保证「多个 goroutine 只读、不并发写同一块数据」,就能无锁并行。
4.2 并行的粒度:按文件 / 按模块切分类型检查
类型检查天然有可并行的结构。一个大型项目由许多源文件组成,很多文件之间的类型检查是相对独立的——检查 a.ts 里一个函数的类型,和检查 b.ts 里另一个函数,大多数时候互不干扰。
概念上的并行调度大致是这样(示意代码,展示思路而非官方实现):
// 把待检查的工作单元(文件/模块)分发到多个 goroutine
func (c *Checker) CheckProgramParallel(files []*SourceFile) []Diagnostic {
numWorkers := runtime.GOMAXPROCS(0) // 通常等于 CPU 核数
jobs := make(chan *SourceFile, len(files))
results := make(chan []Diagnostic, len(files))
var wg sync.WaitGroup
// 启动 worker 池
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for file := range jobs {
// 关键:多个 goroutine 共享同一棵 AST、同一张符号表(只读)
diags := c.checkSourceFile(file)
results <- diags
}
}()
}
// 投递任务
for _, f := range files {
jobs <- f
}
close(jobs)
// 收集
go func() { wg.Wait(); close(results) }()
var all []Diagnostic
for diags := range results {
all = append(all, diags...)
}
return all
}
这段代码的精髓在注释那一行:共享同一棵 AST、同一张符号表(只读)。在 Node.js 里这行注释根本写不出来——你没法让多个 worker 共享同一棵活的 AST。这就是语言级能力差异带来的架构级差异。
4.3 真正的难点:可变共享状态与缓存
当然,现实没这么理想。类型检查器里充满了缓存:类型关系的可赋值性结果会被缓存、getTypeOfSymbol 的结果会被记忆化、条件类型的求值会被复用。这些缓存是可变的共享状态,多个 goroutine 并发写就会数据竞争。
处理这类问题,工程上有几条常见路径,新编译器综合运用了它们:
不可变化(immutability)。 把尽可能多的中间结果设计成一次算出、之后只读。只读数据天然线程安全。
分片(sharding)与本地缓存。 每个 goroutine 维护自己的本地缓存,避免争抢全局锁;只在必要时合并。
细粒度同步。 对确实需要共享的可变结构,用
sync.Map、原子操作或分段锁,而不是一把大锁锁住整个 checker。旧版编译器因为单线程,很多地方是「隐式无锁」的,移植时这些点都得重新审视。确定性输出。 并行带来的一个隐患是输出顺序不稳定。诊断信息必须按稳定顺序排序(比如按文件、按位置),否则同样的代码两次编译报错顺序都不一样,CI diff 会炸。所以并行收集后必然有一步确定性排序。
这也是为什么这次是「移植」而非「重写」却依然极其困难——把一个隐式依赖单线程执行顺序的程序,改造成并行安全的程序,本身就是一场硬仗。 团队花一年多,很大一部分时间就耗在这些并发正确性上。
4.4 语言服务(LSP)也被重写了
除了命令行的 tsc,编辑器体验背后的 TypeScript Language Server 也一并用 Go 重写了。
这一点对日常开发者的体感甚至比 tsc 提速更强烈。旧版语言服务同样跑在 Node 上、单线程,你在大项目里输入代码时的补全延迟、跳转定义卡顿、悬浮提示转圈,根子都在这里。
新版语言服务基于 LSP(Language Server Protocol),用 Go 的并发能力可以:一边响应你的补全请求,一边在后台并行做全项目分析,互不阻塞。前面提到的「VS Code 加载自己代码库 9.6 秒响应」,在新版里被压到了不到 1.2 秒——编辑器可交互时间提升约 8 倍。这不是跑分数字,是你每天敲代码时最真实的爽感来源。
五、代码实战:怎么用上 TypeScript 7.0
理论讲完,上手。
5.1 安装与体验
TypeScript 7.0 通过标准的 typescript 包分发,命令行工具在过渡期叫 tsgo(对应旧版的 tsc):
# 安装 7.0
npm install -D typescript@7
# 用新的原生编译器做类型检查(不产出文件)
npx tsgo --noEmit
# 对比旧版
npx tsc --noEmit
# 看版本
npx tsgo --version
在一个中大型项目里,你大概率会看到类似这样的对比(示意,具体倍数取决于项目结构和 CPU 核数):
$ time npx tsc --noEmit # 旧版
real 0m47.320s
$ time npx tsgo --noEmit # 7.0 原生版
real 0m4.510s
5.2 在 VS Code 里启用原生语言服务
编辑器体验的提升需要显式开启原生语言服务。在 VS Code 设置里:
// settings.json
{
"typescript.experimental.useTsgo": true,
// 指向工作区安装的 7.0 版本
"typescript.tsdk": "node_modules/typescript/lib"
}
开启后,重启 TS Server(命令面板 → "TypeScript: Restart TS Server"),你会发现大项目里的补全、跳转、悬浮提示明显跟手了。
5.3 CI 里的渐进式迁移
不建议一步切换。稳妥的做法是让新旧编译器并行跑一段时间,用旧版做「权威结果」,新版做「验证」,确认诊断一致后再切换:
# .github/workflows/typecheck.yml
name: Type Check
on: [push, pull_request]
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: npm ci
# 权威检查:旧版仍作为门禁
- name: Type check (tsc - authoritative)
run: npx tsc --noEmit
# 影子检查:新版跑一遍,只观察不阻断
- name: Type check (tsgo - shadow)
run: npx tsgo --noEmit
continue-on-error: true
跑上一两周,观察两边诊断是否一致。确认无差异后,把 tsgo 提为权威门禁、删掉旧的 tsc 步骤即可。
5.4 用新的编译器 API(面向工具作者)
如果你写的是 ESLint 插件、代码生成器、类型分析工具这类依赖 TypeScript Compiler API 的东西,这次重写对你影响最大——因为编译器核心不再是 JS,而是 Go 二进制。
TypeScript 团队为此提供了新的 API 访问方式。跨语言调用主要有两条路:
- 通过 LSP / 进程通信。 把编译器当成一个服务进程,用 JSON-RPC(LSP 协议)跟它对话。这是最稳、最推荐的方式,工具用什么语言写都行。
// 概念示意:通过语言服务协议查询类型信息
import { createLanguageServiceClient } from "@typescript/native-api";
const client = await createLanguageServiceClient({
projectPath: "./tsconfig.json",
});
// 查询某个位置的类型(跨进程调用 Go 编译器)
const quickInfo = await client.getQuickInfoAtPosition("src/index.ts", 1024);
console.log(quickInfo.displayString); // e.g. "const x: number"
await client.dispose();
- WebAssembly 编译产物。 Go 可以编译成 Wasm,让编译器核心在浏览器 / 无 Node 环境里跑。这为 Playground、在线 IDE、Serverless 类型检查打开了新空间。
需要注意:旧版那种「直接 import * as ts from "typescript" 然后同步遍历 AST」的用法,在 7.0 里会逐步收敛到通过 API 层调用。 过渡期两套 API 会并存,但长期看,依赖内部实现细节的工具需要按新 API 迁移。这是这次重写对生态最大的一次「税」。
六、性能优化:拆开那 10 倍到底从哪来
把提速拆成可归因的几块,心里会更有数:
| 来源 | 贡献量级 | 原理 |
|---|---|---|
| 原生编译(去 JIT / 冷启动) | ~2× | Go 二进制启动即原生速度,tsc 短命进程不再等 V8 预热 |
| 紧凑内存布局 + 值类型 | ~1.5× | struct/切片连续内存,缓存命中率高,GC 压力小 |
| 共享内存并行 | 多 goroutine 在共享 AST 上并行类型检查,吃满多核 | |
| 更省的 GC 抖动 | ~1.2× | Go GC 低延迟并发回收,配合值类型减少堆分配 |
这些不是简单相乘,但叠加效应能解释「8~12 倍」这个区间:核数越多、项目越大,并行那一块的收益越显著——所以官方说的是一个区间而非固定数字。在 4 核笔记本上可能是 6 倍,在 32 核构建机上冷构建可能逼近甚至超过 12 倍。
几条对你有实操意义的优化认知:
核数现在真的能吃满了。 以前给 CI 加核对
tsc几乎没用;现在加核直接换来线性提速(在并行度饱和前)。CI 机器选型逻辑变了。增量构建依然重要,但没那么救命了。 旧版因为全量太慢,大家高度依赖
--incremental和 project references 来省时间。新版全量本身就快,增量的边际收益下降,架构上可以适当简化。内存峰值明显下降。 紧凑布局让大 monorepo 的类型检查内存占用大幅降低,以前 OOM 的项目现在能过。
别拿冷启动的第一次跑分下结论。 文件系统缓存、OS 调度都会影响第一次。测提速要多跑几次取稳定值。
七、迁移踩坑清单(一线视角)
理想很丰满,真迁移时会遇到这些现实问题:
依赖编译器内部 API 的工具会先崩。 检查你的
ts-node、ts-jest、各种ts-*transformer、自定义 ESLint 规则里有没有直接import "typescript"深挖内部 AST。这些是迁移路上最先炸的雷。等待它们发布兼容 7.0 的版本,或走新 API。诊断顺序可能变化。 并行导致的输出顺序问题理论上被确定性排序兜住了,但如果你的脚本 grep 错误信息、或对错误顺序有隐式依赖,值得回归验证一遍。
tscvstsgo命令名过渡。 过渡期两个命令并存,脚本里写死tsc的地方要评估何时切tsgo。别在没验证一致性前全局替换。Emit 行为的极端边界。 虽然目标是语义一致,但产出的
.js/.d.ts在格式化、注释保留等非语义层面可能有微小差异。如果你对 emit 产物做过字节级 diff 校验,要重新建立基线。老旧 Node 版本。 原生二进制对宿主 Node 依赖降低,但工具链集成仍需较新的 Node,升级前确认 CI/本地环境版本。
别指望它修类型报错。 提速归提速,7.0 的类型判定和 6.0 一致——旧版报错的地方新版照样报,这是设计目标,不是 bug。
一句忠告:先影子运行,再切权威门禁。 前面 CI 那套「双跑对比」流程就是为这个准备的,别跳。
八、生态影响:这不只是 TypeScript 变快了
往大了看,TypeScript 7.0 是「前端工具链原生化」浪潮里分量最重的一块拼图。
这几年的趋势线非常清晰:性能敏感的开发工具,正在集体逃离 JavaScript 运行时。 esbuild(Go)、SWC(Rust)、Rome/Biome(Rust)、Oxc(Rust)、Rolldown(Rust)、Turbopack(Rust)……打包器、压缩器、linter、formatter 一个接一个用原生语言重写。而 TypeScript 编译器是这条路上最难啃、也最有象征意义的一块骨头——因为它是整个类型生态的根。
它落地后带来几个连锁反应:
「类型检查慢」这个大型项目的经典痛点,被系统性解决。 以后没人再有借口说「因为太慢所以 CI 不跑全量类型检查」。
编辑器体验拉齐了原生 IDE。 TypeScript 一直被诟病大项目里补全跟不上手,这次直接对标 JetBrains 系原生 IDE 的响应速度。
跨语言工具集成成为常态。 编译器变成一个可以被任意语言通过协议调用的服务,Rust/Go/Python 写的工具都能干净地接入 TS 类型信息,而不用尴尬地嵌一个 Node 进去。
给「用原生语言重写基础设施」这件事盖章。 当微软自己都把旗舰工具从 TS 迁到 Go,「JS 工具链原生化」就从社区实验变成了行业共识。
有一点值得冷静:这不代表 JavaScript 不行了。 恰恰相反,这是分工的成熟——应用逻辑继续用 TS/JS 写(生态、迭代速度无可替代),而性能敏感的底层工具用原生语言写。 两者各司其职,而不是谁取代谁。TypeScript 作为「语言」比以往任何时候都更强势,只是它的「编译器」换了副更结实的骨架。
九、总结与展望
把这次重写浓缩成几句话:
- 它是移植,不是重写。 目标是语义 100% 一致、复用十年测试套件,而不是重新设计语言行为。这个约束决定了一切后续选型。
- 选 Go 是约束满足的最优解。 GC 让循环引用对象图能逐行翻译,struct 给了内存布局控制权,goroutine + 共享堆内存实现真并行,原生编译干掉 JIT 冷启动。Rust 太逆着 AST 数据结构,C# 分发和内存控制不够顺,无 GC 语言移植成本太高。
- 10× 提速 = 原生编译 × 紧凑内存 × 共享内存并行。 其中并行是核数越多收益越大的那一块,也是 JS 运行时结构性做不到的。
- 最难的不是翻译代码,是把隐式单线程程序改造成并发安全程序。 一年多的工期大头花在这里。
- 对开发者:
tsc/CI 提速立竿见影,编辑器体验提升最爽;对工具作者:编译器 API 是最大的迁移成本,要走新 API / LSP。
往前看,几个值得关注的方向:一是 Wasm 版编译器 会把「浏览器里跑完整类型检查」变成现实,Playground 和在线 IDE 会脱胎换骨;二是并行度还有上限可挖——目前主要在文件/模块粒度并行,更细粒度(单文件内类型求值的并行)是下一个战场;三是编译器即服务的模式会催生一批过去因为性能不可行的工具(实时全项目重构、大规模 codemod、AI 辅助的类型级分析)。
TypeScript 用一次「换骨不换皮」的移植,给所有做基础设施的人上了一课:当你的工具成了整个生态的性能天花板时,最勇敢也最负责任的选择,是把地基刨了重浇——但要小心翼翼地保证房子里的东西一件都没动。
十四年前,TypeScript 用「能编译自己」证明了自己的成熟;十四年后,它用「不再需要编译自己」证明了自己的自信。这中间的距离,就是一门语言从「证明可行」走到「追求极致」的全部旅程。
本文基于 TypeScript 7.0 正式版公开信息与编译器工程通用原理撰写,文中并行调度、数据结构等代码为阐述架构思路的示意实现,非官方源码;具体 API 与命令请以官方文档为准。