从万米高空俯瞰 TypeScript 编译器:架构与代码库导览
前置知识
- ›对编译器基本概念有所了解(词法分析、语法解析、AST、类型系统)
- ›具备 TypeScript 语言的日常使用经验
- ›理解 JavaScript 闭包与模块模式
从万米高空俯瞰 TypeScript 编译器:架构与代码库导览
TypeScript 6.0 是 TypeScript 编译器在团队着手用 Go 重写之前,最后一个基于 JavaScript 实现的版本。这使得这份代码库具有了里程碑式的意义——它是一套服务了数百万开发者超过十年的编译器架构的成熟终态。当然,它也是近 10 万行密密麻麻、大量使用闭包的 TypeScript 代码,没有一张地图,很容易迷失其中。本文就是那张地图。
我们将依次浏览代码库的目录结构、梳理三个入口点、预览五阶段编译流水线、介绍贯穿全局的三个核心数据结构、理解分层架构,并了解构建工具如何将一切串联起来。
代码库布局与子项目结构
TypeScript 的源代码位于 src/ 目录下,以一组 TypeScript 项目引用(project references)的形式组织。根配置文件 src/tsconfig.json 链接了十三个子项目:
graph TD
ROOT["src/tsconfig.json"] --> compiler["src/compiler"]
ROOT --> services["src/services"]
ROOT --> server["src/server"]
ROOT --> tsc["src/tsc"]
ROOT --> tsserver["src/tsserver"]
ROOT --> typescript["src/typescript"]
ROOT --> deprecatedCompat["src/deprecatedCompat"]
ROOT --> harness["src/harness"]
ROOT --> jsTyping["src/jsTyping"]
ROOT --> testRunner["src/testRunner"]
ROOT --> typingsInstaller["src/typingsInstaller"]
ROOT --> typingsInstallerCore["src/typingsInstallerCore"]
ROOT --> watchGuard["src/watchGuard"]
| 目录 | 职责 | 大致行数 |
|---|---|---|
src/compiler |
核心编译器:scanner、parser、binder、checker、emitter 及各类 transformer | ~80,000 行 |
src/services |
语言服务:补全、诊断、重构、代码修复 | ~25,000 行 |
src/server |
tsserver:协议处理、会话管理、项目管理 | ~15,000 行 |
src/tsc |
tsc CLI 入口点 |
24 行 |
src/tsserver |
tsserver CLI 入口点 |
57 行 |
src/typescript |
公共 API 入口点(npm 包) | 25 行 |
理解这个代码库的关键在于 barrel 文件 src/compiler/_namespaces/ts.ts。该文件按依赖顺序重新导出所有编译器模块——从 corePublic.js 开始,依次经过 scanner.js、parser.js、binder.js、checker.js、各个 transformer,再到 emitter.js,最后是 program.js。这个文件中的导出顺序,本身就是编译流水线的依赖关系图。
提示: 当你在代码库中迷失方向时,回到
src/compiler/_namespaces/ts.ts是最快的找到出路的方式。导出顺序清晰地告诉你各模块之间的依赖关系,以及任何给定概念在流水线中所处的位置。
入口点:tsc、tsserver 与公共 API
TypeScript 有三个入口点,它们都出奇地精简——这是一个刻意为之的设计决策,目的是让核心逻辑保持高度可复用性。
flowchart LR
tsc["src/tsc/tsc.ts<br/>(24 lines)"] --> ecl["executeCommandLine()"]
tsserver["src/tsserver/server.ts<br/>(57 lines)"] --> session["Server Session"]
typescript["src/typescript/typescript.ts<br/>(25 lines)"] --> api["Public ts.* API"]
ecl --> compiler["Compiler Core"]
session --> ls["Language Service"]
ls --> compiler
api --> compiler
tsc CLI 位于 src/tsc/tsc.ts,总共只有 24 行。它配置调试日志、在开发模式下启用 source map、将 stdout 设为阻塞模式,然后调用 ts.executeCommandLine(ts.sys, ts.noop, ts.sys.args)。这一行代码将执行权分发到 src/compiler/executeCommandLine.ts,在那里会根据场景选择走 performBuild(用于 tsc -b 项目构建)还是 executeCommandLineWorker(用于普通编译)。
tsserver 进程位于 src/tsserver/server.ts,负责初始化 Node.js 运行环境——包括重写 console.log 以将输出重定向到 logger(防止插件污染 stdio 协议)——并根据命令行参数启动一个服务器会话。
npm 包入口位于 src/typescript/typescript.ts,设置废弃警告的日志 host 后,将整个 ts 命名空间重新导出。这就是你执行 import * as ts from "typescript" 时所得到的内容。package.json 将 main 字段指向 lib/typescript.js,并为 tsc 和 tsserver 暴露了对应的 bin 入口。
五阶段编译流水线
TypeScript 的核心是一套经典的多遍编译流水线。源代码文本依次流经五个独立的阶段,每个阶段都在上一阶段的基础上构建出更丰富的表示:
flowchart LR
Source["Source Text"] --> Scanner
Scanner -->|"SyntaxKind tokens"| Parser
Parser -->|"AST (SourceFile)"| Binder
Binder -->|"Symbols + FlowNodes"| Checker
Checker -->|"Types + Diagnostics"| Emitter
Emitter -->|".js / .d.ts / .map"| Output["Output Files"]
| 阶段 | 文件 | 行数 | 核心入口 |
|---|---|---|---|
| Scanner | scanner.ts |
~4,100 | createScanner() |
| Parser | parser.ts |
~10,800 | createSourceFile() |
| Binder | binder.ts |
~3,900 | bindSourceFile() |
| Checker | checker.ts |
~54,400 | createTypeChecker() |
| Emitter | emitter.ts |
~6,400 | emitFiles() |
Scanner 将源代码文本词法化为 SyntaxKind token 流。Parser 通过递归下降消费这些 token,构建出 SourceFile AST。Binder 遍历 AST,创建 Symbol 对象并构建控制流图。Checker 是整个流水线中体量最大的部分(超过 54,000 行),负责执行类型检查、类型推断和可赋值性分析。Emitter 通过一系列语法降级 pass 对 AST 进行转换,并将结果序列化为 JavaScript 文件、声明文件和 source map。
每个阶段都以基于闭包的模块形式实现。仅 checker 一个模块,就在 createTypeChecker() 内部声明了数以百计的 var 局部变量。这并非偶然——代码库中有一段明确的性能注释:
// Why var? It avoids TDZ checks in the runtime which can be costly.
// See: https://github.com/microsoft/TypeScript/issues/52924
我们将在后续文章中深入探讨这一模式。
三大核心数据结构:Node、Symbol、Type
三个接口构成了整个系统的基础。理解它们各自在何时、何处被创建,是读懂这份代码库的关键所在。
classDiagram
class Node {
+kind: SyntaxKind
+flags: NodeFlags
+parent: Node
+pos: number
+end: number
}
class Symbol {
+flags: SymbolFlags
+escapedName: __String
+declarations: Declaration[]
+members: SymbolTable
+exports: SymbolTable
}
class Type {
+flags: TypeFlags
+symbol: Symbol
+aliasSymbol: Symbol
+aliasTypeArguments: Type[]
}
Node --> Symbol : "declaration.symbol"
Symbol --> Type : "via SymbolLinks.type"
Type --> Symbol : "type.symbol"
Node(src/compiler/types.ts#L942-L955)是 AST 节点。每个节点都有一个 kind(来自庞大的 SyntaxKind 枚举)、flags(元数据,如 let/const、导出状态、parser 上下文)、一个 parent 指针(在 binding 阶段设置)以及文本范围(pos/end)。节点由 parser 创建,创建后不可变更。
Symbol(src/compiler/types.ts#L6037-L6054)是声明的语义身份标识。Symbol 由 binder 创建。一个 symbol 持有 flags(将其分类为 Variable、Function、Class、Interface 等)、escapedName,以及 declarations、members 和 exports 数组。当多个声明共用同一名称时(例如同一接口被声明两次),它们会合并为单个 symbol。
Type(src/compiler/types.ts#L6439-L6455)是类型系统的内部表示。Type 由 checker 按需懒惰地创建。一个 type 持有 flags(将其分类为 String、Number、Object、Union、Intersection、Conditional 等)、一个指向其 symbol 的反向引用,以及用于泛型类型解析的缓存形式,如 permissiveInstantiation 和 restrictiveInstantiation。
数据在流水线各阶段之间流动:parser 创建 Node,binder 将 Symbol 附加到声明 Node 上,checker 则通过 SymbolLinks 旁路通道从 Symbol 懒惰地计算出 Type。
分层架构:Compiler → Services → Server
代码库被组织为三个架构层次,每一层都建立在下一层之上:
flowchart TB
subgraph "Layer 3: Server"
tsserver["tsserver entry"]
session["Session (protocol dispatch)"]
projService["ProjectService (multi-project mgmt)"]
end
subgraph "Layer 2: Language Service"
ls["LanguageService API"]
completions["Completions"]
diagnostics["Diagnostics"]
refactors["Refactors / Codefixes"]
end
subgraph "Layer 1: Compiler Core"
program["Program"]
checker["Type Checker"]
emitter["Emitter"]
parser["Parser"]
scanner["Scanner"]
binder["Binder"]
end
tsserver --> session
session --> projService
projService --> ls
ls --> program
program --> checker
program --> emitter
program --> parser
parser --> scanner
program --> binder
completions --> checker
diagnostics --> checker
refactors --> checker
第一层(编译器核心)——src/compiler/——提供纯粹的编译流水线。Program 作为编排者负责文件解析、管理 checker 的生命周期并协调 emit 过程。这一层完全不了解编辑器或服务器的存在。
第二层(语言服务)——src/services/——用面向编辑器的 API 对 Program 进行封装。src/services/services.ts 中的 createLanguageService() 提供了 getCompletionsAtPosition、getSemanticDiagnostics、getDefinitionAtPosition、findReferences 等方法。这一层还通过声明合并为 AST 节点添加了便捷方法,例如 node.getSourceFile() 和 node.getChildren()。
第三层(服务器)——src/server/——通过 JSON wire 协议对外暴露语言服务。src/server/session.ts 中的 Session 类将传入的请求(补全、诊断、重命名等)分发到对应的 LanguageService 方法。src/server/editorServices.ts 中的 ProjectService 负责管理工作区内的多个项目——包括配置项目(来自 tsconfig.json)、推断项目(针对散落的文件)以及外部项目(来自构建工具)。
构建系统与诊断信息生成
TypeScript 使用 hereby 作为任务运行器,使用 esbuild 进行打包。Herebyfile.mjs 定义了所有构建任务。
flowchart LR
diagnosticMessages["diagnosticMessages.json"] -->|"generate-diagnostics"| generated["diagnosticInformationMap.generated.ts"]
generated --> compile["build-src (tsc --build)"]
compile -->|"emitDeclarationOnly"| dts[".d.ts files"]
dts --> bundle["esbuild bundler"]
bundle --> lib["lib/typescript.js<br/>lib/tsc.js<br/>lib/tsserver.js"]
构建流程采用了一种巧妙的两阶段设计。src/tsconfig-base.json 中将 TypeScript 编译器配置为 emitDeclarationOnly: true——TypeScript 只负责生成声明文件和声明 map,实际的 JavaScript 打包工作则交给 esbuild 完成。这样既获得了 esbuild 在 JS 输出方面的速度优势,又保留了 tsc 完整的类型检查能力。
Herebyfile.mjs 中的 esbuild 配置以 CJS 格式为目标,编译目标为 es2020 + node14.17,将所有内容打包进单个文件,并对 typescript.js 包应用了一个巧妙的包装器——将整个模块包裹在 var ts = {}; ((module) => { ... })({ get exports() { return ts; } }) 中,使得 ts 全局变量既能在 CJS 消费者环境中正常工作,也兼容 Monaco 的 ESM 打包方式。
诊断信息以 src/compiler/diagnosticMessages.json 作为唯一数据源,每条消息都有一个人类可读的 key、一个类别(Error、Warning、Message、Suggestion)以及一个稳定的数字代码。通过代码生成步骤,这份 JSON 被转换为 diagnosticInformationMap.generated.ts,为编译器提供带有预填充类别和代码字段的类型化常量,例如 Diagnostics.Unterminated_string_literal。
提示: 当你看到一个 TypeScript 错误代码(如
TS1002)时,可以在diagnosticMessages.json中搜索"code": 1002找到对应的消息模板,然后 grep 其生成的常量名称,就能定位到 checker 或 parser 中报告该错误的具体位置。
下一步
有了这张全局地图,你已经做好了深入细节的准备。在第二篇文章中,我们将进入编译器的前端——Scanner 与 Parser——探究原始源文本是如何一步步转化为结构化 AST 的。我们将梳理对所有可能节点进行分类的 SyntaxKind 枚举、剖析基于闭包的 scanner 实现,并观察递归下降 parser 如何从 token 流构建出一个完整的 SourceFile。