Read OSS

Emitter 流水线:转换、代码生成与声明文件

高级

前置知识

  • 第 1-4 篇:完整的编译器流水线,直至类型检查阶段
  • 理解 JavaScript 模块系统(CommonJS、ESM、AMD)
  • 熟悉 source map 与代码生成的基本概念

Emitter 流水线:转换、代码生成与声明文件

扫描、解析、绑定、类型检查完成之后,编译器的最后一项工作是生成输出:JavaScript 文件、声明文件(.d.ts)以及 source map。Emitter 流水线并不是直接遍历 AST 然后拼接字符串——它依赖的是转换机制:一条由 AST 到 AST 的处理链,逐步将 TypeScript 和现代 JavaScript 语法降级到目标输出级别,最后再由 printer 将最终的 AST 序列化为文本。

这一架构是 TypeScript 编译器设计中最精妙的部分之一。每个转换器只负责一件事(去除类型、降级 class 字段、转换 JSX、输出 CommonJS 模块),由于它们都基于同一套 AST 表示,因此可以干净地组合在一起。

emitFiles 与转换器链

Emit 流水线的入口是 src/compiler/emitter.ts 中的 emitFiles()。它接收一个 EmitResolver(checker 面向 emit 的 API)、一个 EmitHost、一个包含 script 转换器和声明转换器的 EmitTransformers 对象,以及若干标志位。

flowchart LR
    Program["program.emit()"] --> EF["emitFiles()"]
    EF --> GT["getTransformers(options)"]
    GT --> Chain["Transformer Chain"]
    Chain --> T1["transformTypeScript"]
    T1 --> T2["transformJsx"]
    T2 --> T3["transformESNext"]
    T3 --> T4["transformClassFields"]
    T4 --> T5["transformES20xx"]
    T5 --> T6["transformModule"]
    T6 --> Printer["Printer → text output"]
    Printer --> JS[".js file"]
    Printer --> Map[".js.map file"]
    
    EF --> DT["Declaration Transforms"]
    DT --> TD["transformDeclarations"]
    TD --> DPrinter["Printer → .d.ts output"]

src/compiler/transformer.ts 中的 getTransformers() 函数根据编译器选项组装有序的转换器链。顺序至关重要——每个转换器都预期源码中某些语法仍然存在,后续的转换器也依赖前面的转换器已经完成了对目标语法的降级处理。

function getScriptTransformers(compilerOptions, customTransformers, emitOnly) {
    const transformers = [];
    addRange(transformers, customTransformers?.before);

    transformers.push(transformTypeScript);  // Always first: strip types

    if (compilerOptions.experimentalDecorators) {
        transformers.push(transformLegacyDecorators);
    }
    if (getJSXTransformEnabled(compilerOptions)) {
        transformers.push(transformJsx);
    }
    // ESNext, ESDecorators, ClassFields...
    // Then progressive downleveling: ES2021 → ES2020 → ... → ES2015
    if (languageVersion < ScriptTarget.ES2015) {
        transformers.push(transformES2015);
        transformers.push(transformGenerators);
    }

    transformers.push(getModuleTransformer(moduleKind));  // Always last

    addRange(transformers, customTransformers?.after);
    return transformers;
}

提示: 来自 ts-patch 或 ttypescript 等构建工具的自定义转换器插件,通过 customTransformers.beforecustomTransformers.after 数组接入流水线。要编写能与内置转换器正确协作的插件,理解这条链的顺序至关重要。

核心转换器:类型剥离、模块、JSX 与降级

TypeScript → JavaScript 转换

transformTypeScript 始终是链中的第一个转换器,负责:

  • 剥离类型注解:类型引用、类型断言、as 表达式、仅类型的 import/export——全部移除
  • 转换枚举enum Color { Red, Green, Blue } 会变成一个构建对象的 IIFE,同时包含正向和反向映射
  • 转换命名空间namespace Foo { ... } 会变成一个填充 Foo 对象的 IIFE
  • 处理参数属性constructor(public x: number) 会生成对应的 this.x = x 赋值语句
  • 移除 declare:环境声明不产生任何输出

模块转换

getModuleTransformer() 中的模块转换器选择逻辑根据 moduleKind 进行分发:

flowchart TD
    MK["moduleKind"] --> Preserve["Preserve → transformECMAScriptModule"]
    MK --> Node["ESNext/ES2022/ES2020/ES2015/Node16/Node18/Node20/NodeNext/CommonJS → transformImpliedNodeFormatDependentModule"]
    MK --> System["System → transformSystemModule"]
    MK --> Default["AMD/UMD/Default → transformModule"]

transformImpliedNodeFormatDependentModule 尤为有趣——它同时封装了 transformModule(CJS 输出)和 transformECMAScriptModule(ESM 输出),根据每个文件的 impliedNodeFormat 在两者之间切换。这正是 --module nodenext 能在同一次编译中为 .cts 文件输出 CJS、为 .mts 文件输出 ESM 的实现原理。

主模块转换器 transformModule 负责处理 CommonJS、AMD 和 UMD 输出,将 import/export 语句转换为 require() 调用、Object.defineProperty(exports, ...) 赋值,以及 AMD/UMD 的工厂函数包装。

JSX 转换

transformJsx 将 JSX 语法转换为函数调用。使用 --jsx react 时,<div className="x"> 会变成 React.createElement("div", { className: "x" });使用 --jsx react-jsx 时,则变成 _jsx("div", { className: "x" }),并自动导入 JSX runtime。

Class 字段与装饰器

transformClassFields 处理 class 字段声明的降级细节,包括 useDefineForClassFields[[Define]] vs [[Set]] 语义之间的交互。transformESDecorators 实现了 TC39 stage 3 装饰器提案,而 transformLegacyDecorators 则处理旧版 --experimentalDecorators 的行为。

ES 降级

transformES20xx 系列转换器负责将现代语法逐步降级到更旧的编译目标:

转换器 降级内容
transformESNext 尚未进入稳定目标的最新提案
transformES2021 逻辑赋值运算符(??=||=&&=
transformES2020 可选链(?.)、空值合并(??
transformES2019 Array.flat、可选 catch 绑定
transformES2018 异步迭代、对象 rest/spread
transformES2017 async/await → Promise 链
transformES2016 幂运算符(**
transformES2015 箭头函数、class、解构、for-of、模板字符串
transformGenerators Generator 函数 → 状态机

每个转换器仅在目标版本低于对应 ES 级别时才会被加入。将目标设为 ES2020 时,ES2020 及以上的所有转换器都会被跳过。

声明文件生成

声明文件(.d.ts)由独立的转换器链生成。transformDeclarations 遍历 AST 并执行以下操作:

  1. 移除实现体:函数体替换为 ;,class 方法体被删除
  2. 保留类型签名:参数类型、返回类型、属性类型、泛型约束
  3. 解析推断类型:当函数缺少返回类型注解时,转换器会回调 checker 计算推断类型,再通过 expressionToTypeNode 将其具象化为 AST 语法节点
  4. 按可见性过滤:输出中只保留导出的声明
  5. 处理重导出export { Foo } from './bar' 需要追溯到原始声明
flowchart TD
    SF["SourceFile AST"] --> DT["transformDeclarations"]
    DT --> Strip["Strip function bodies"]
    DT --> Visibility["Filter: only exported"]
    DT --> Infer["Resolve inferred types"]
    Infer --> E2TN["expressionToTypeNode()"]
    E2TN --> Checker["Checker: getTypeOfSymbol()"]
    Checker --> TypeNode["TypeNode AST"]
    DT --> DTS[".d.ts SourceFile"]
    DTS --> Printer["Printer → .d.ts text"]

expressionToTypeNode 这座桥梁(在第 4 篇中已有介绍)在这里不可或缺。来看一个例子:

export function getConfig() {
    return { debug: true, level: 3 };
}

源码中没有返回类型注解,但 .d.ts 必须包含一个:export function getConfig(): { debug: boolean; level: number; }。声明转换器会向 checker 请求推断的返回类型,再将那个内部的 Type 对象转换回 AST 类型节点。

提示: 如果你在排查声明 emit 的问题(尤其是涉及 isolatedDeclarations 的场景),追踪路径是:transformDeclarations → checker 回调 → expressionToTypeNode。TypeScript 5.5 引入的 isolatedDeclarations 特性对此模式加以限制,要求显式添加类型注解,从而使声明 emit 完全不依赖 checker。

Emit Helpers 与 Source Map

运行时 Helpers

转换器在降级语法时,有时需要运行时辅助函数。例如,降级 async/await 需要 __awaiter,降级 class 继承需要 __extends,降级 rest 参数需要 __rest

src/compiler/factory/emitHelpers.ts 模块定义了这些 helpers。每个 helper 都是一个 EmitHelper 对象,包含名称、文本(helper 函数体)以及表示其作用域的标志位。

引入 helpers 有两种策略:

  1. 内联:将 helper 函数直接输出到每个需要它的文件中,这是默认行为。
  2. 外部引入(--importHelpers:输出文件从 tslib npm 包中导入 helpers,避免在大量输出文件中重复相同的 helper 代码。

转换器通过 context.requestEmitHelper() 请求所需的 helper,emitter 在序列化过程中统一收集。

Source Map 生成

src/compiler/sourcemap.ts 模块负责 source map 的生成,追踪原始 TypeScript 源码位置与输出 JavaScript 位置之间的映射关系。Printer 遍历转换后的 AST 并写入输出文本时,会为每个带有原始位置的节点记录 source map 条目。

Source map 生成器输出标准的 V3 格式 source map,映射信息采用 Base64 VLQ 编码。Source map 可以作为独立的 .js.map 文件输出(--sourceMap),也可以作为 data URI 内联到 JavaScript 输出中(--inlineSourceMap)。对于声明文件,--declarationMap 会生成 .d.ts.map 文件,将 .d.ts 映射回原始的 .ts 源码——这对于从 .d.ts 跳转到真实源码的"Go to Definition"功能至关重要。

flowchart LR
    Original["Original AST<br/>(with positions)"] --> Transforms["Transform Chain"]
    Transforms --> Transformed["Transformed AST<br/>(with original pointers)"]
    Transformed --> Printer["Printer"]
    Printer --> JS["JavaScript Text"]
    Printer --> SMG["SourceMapGenerator"]
    SMG --> SM[".js.map<br/>(VLQ mappings)"]

这里有一个关键设计:每个转换后的节点都带有一个 original 指针,指回它所派生的源节点(由 NodeFactory.update* 系列方法负责设置)。Printer 利用这些指针在 source map 中记录正确的源码位置——即便 AST 结构在经历一系列转换后已经面目全非。

下一步

至此,我们已经完整追踪了整条编译流水线——从源码文本,经过扫描、解析、绑定、类型检查、转换,最终到达代码输出。但 TypeScript 编译器不只是一个批处理工具,它同样驱动着你每天使用的所有 IDE 功能。在第 6 篇中,我们将深入探索 Language Service 与 tsserver——自动补全、跳转定义、查找引用、重构和代码修复是如何构建在编译器核心之上的,以及 server 进程如何在一个工作区中管理多个项目。