Read OSS

从万米高空俯瞰 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.jsparser.jsbinder.jschecker.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.jsonmain 字段指向 lib/typescript.js,并为 tsctsserver 暴露了对应的 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"

Nodesrc/compiler/types.ts#L942-L955)是 AST 节点。每个节点都有一个 kind(来自庞大的 SyntaxKind 枚举)、flags(元数据,如 let/const、导出状态、parser 上下文)、一个 parent 指针(在 binding 阶段设置)以及文本范围(pos/end)。节点由 parser 创建,创建后不可变更。

Symbolsrc/compiler/types.ts#L6037-L6054)是声明的语义身份标识。Symbol 由 binder 创建。一个 symbol 持有 flags(将其分类为 Variable、Function、Class、Interface 等)、escapedName,以及 declarationsmembersexports 数组。当多个声明共用同一名称时(例如同一接口被声明两次),它们会合并为单个 symbol。

Typesrc/compiler/types.ts#L6439-L6455)是类型系统的内部表示。Type 由 checker 按需懒惰地创建。一个 type 持有 flags(将其分类为 String、Number、Object、Union、Intersection、Conditional 等)、一个指向其 symbol 的反向引用,以及用于泛型类型解析的缓存形式,如 permissiveInstantiationrestrictiveInstantiation

数据在流水线各阶段之间流动: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() 提供了 getCompletionsAtPositiongetSemanticDiagnosticsgetDefinitionAtPositionfindReferences 等方法。这一层还通过声明合并为 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