编程 TypeScript 编译器 API 实战:写一个自定义 Transformer 改写 AST

2026-10-03 00:04:40

TypeScript 编译器 API 实战:写一个自定义 Transformer 改写 AST

TypeScript 允许把代码从一种形式 transform 到另一种形式,做的事情和 Babel 的 plugin 类似。项目地址:itsdouges/typescript-transformer-handbook。

什么是 AST

AST(抽象语法树)是描述已解析代码的数据结构。调 AST 时建议打开 ts-ast-viewer.com 观察。下面这段代码:

function hello() {
console.log('world');
}

对应的 AST 表示:

-> SourceFile
-> FunctionDeclaration
- Identifier
-> Block
-> ExpressionStatement
-> CallExpression
-> PropertyAccessExpression
- Identifier
- Identifier
- StringLiteral
- EndOfFileToken

每个节点都有 kind 属性(SyntaxKind 枚举值)描述节点类型,以及 pos / end 描述它在源码中的位置。

编译阶段(Stages)

TypeScript 一共五个阶段:parser、binder、checker、transform、emitting。其中 binder 和 checker 是 TS 特有的。

一个 Program 是一组入口源文件消费一个或多个模块的集合,整个集合在每个阶段都会被用到。这点和 Babel 不同:Babel 是 file in file out,TypeScript 是 project in, project out。这也是 Babel 解析 TypeScript 时 enum 不能工作的原因——它拿不到全部信息。

  • Parser:实际分两部分,scanner 和 parser。SourceCode ~~ scanner ~~> Token Stream ~~ parser ~~> AST。scanner 把字符串线性转成 token,parser 负责树化。
  • Binder:创建 symbol map,基于 AST 提供类型系统,用来链接引用、知道 import/export 的节点。
  • Transforms:写 transformer 的阶段,可以任意改代码。
  • Emitting:最后阶段,把最终代码写到某处(通常是文件系统,也可能是内存)。

三个 transform 阶段

  • before —— 在 TypeScript 自己的 transformer 之前运行(代码还没编译)
  • after —— 在 TypeScript 自己的 transformer 之后运行(代码已编译)
  • afterDeclarations —— 在 declaration 阶段之后运行(可以改类型定义)

90% 的情况写 before。需要编译后处理或改类型时用 after / afterDeclarations。

transformer 之后不应该再做类型检查,如果发生多半是 bug。

遍历(Traversal)

TS 提供两个主要方法:

import * as ts from 'typescript';

ts.visitNode(sourceFile, visitor, test);
ts.visitEachChild(node, visitor, context);

visitor 模式每次写 transformer 都会用到。最简单的 visitor:

import * as ts from 'typescript';

const transformer = sourceFile => {
const visitor = (node: ts.Node): ts.Node => {
console.log(node.kind, `\t# ts.SyntaxKind.${ts.SyntaxKind[node.kind]}`);
return ts.visitEachChild(node, visitor, context);
};
return ts.visitNode(sourceFile, visitor, ts.isSourceFile);
};

必须 return 每个节点,否则会出怪错。

context 是每个 transformer 都会收到的转换上下文,除了给 visitEachChild 用,还能拿到当前 TypeScript 配置。

Transformer API

写 transformer 用 TypeScript,主要靠 typescript 包。安装 npm i typescript --save,然后 import * as ts from 'typescript'。

Visiting:

  • ts.visitNode(node, visitor, test)
  • ts.visitEachChild(node, visitor, context)
  • ts.isXyz(node) —— 收窄 node 类型,如 ts.isVariableDeclaration(node)

Nodes:

  • ts.factory.createXyz(...) —— 创建新节点,如 ts.factory.createIdentifier('world')
  • ts.factory.updateXyz(node, ...) —— 更新节点,如 ts.factory.updateVariableDeclaration()
  • ts.factory.updateSourceFile(sourceFile, ...)
  • ts.setOriginalNode(newNode, originalNode)
  • ts.setXyz(...) / ts.addXyz(...) —— 设置/添加

context 上常用:

  • getCompilerOptions()
  • hoistFunctionDeclaration(node)
  • hoistVariableDeclaration(node)

program(写 Program transformer 时可用的特殊属性):

  • getRootFileNames() / getSourceFiles() / getCompilerOptions()
  • getSourceFile(fileName) / getSourceFileByPath(path)
  • getCurrentDirectory()
  • getTypeChecker()

typeChecker(program.getTypeChecker() 的结果):

  • getSymbolAtLocation(node)
  • getExportsOfModule(symbol)

写第一个 transformer

import * as ts from 'typescript';

const transformer: ts.TransformerFactory = context => {
return sourceFile => {
return sourceFile;
};
};

export default transformer;

transformer factory 能拿到 context。接着加 visitor 遍历每个节点,再找 Identifier 改名。源:

babel === plugins;

改写后:

typescript === transformers;

完整逻辑:ts.isIdentifier(node) 判断,node.escapedText 匹配,return ts.factory.createIdentifier('typescript') 替换。

transformer 的类型

  • Factory:ts.TransformerFactory,通过 context 拿到配置,最常见。
  • Config:从 tsconfig 读自定义配置。
  • Program:能拿到整个 program 的信息(所有文件、typeChecker 等)。

怎么消费 transformer

官方 tsc 命令不支持直接加载自定义插件,但有几种办法:

  1. 直接调用 TS 编译器 API 编译代码。
  2. 用社区的 TTypeScript 项目:cevek/ttypescript。
  3. Webpack + ts-loader,配置 getCustomTransformers:
{
test: /\.ts$/,
loader: 'ts-loader',
options: {
getCustomTransformers(program) {
return {
before: [myTransformer],
after: []
};
}
}
}

Parcel 也有对应支持。

转换操作(Transformation operations)

遍历相关:

  • 判断节点类型:ts.isXyz(node) 或 node.kind === ts.SyntaxKind.xxx
  • 判断两个 identifier 是否指向同一个 symbol:typeChecker.getSymbolAtLocation()
  • 找特定父节点:向上遍历 node.parent
  • 停止遍历:返回当前节点,而不是 visitEachChild

操作节点:

  • 更新节点:ts.factory.updateXyz
  • 替换节点:visitor 返回新节点
  • 一个节点替换成多个:返回数组
  • 插入兄弟节点:ts.factory.createXyz 后插到父节点 statements 里
  • 删除节点:返回 undefined 或过滤数组
  • 添加新 import 声明:创建 ImportDeclaration 加到 sourceFile.statements 开头

作用域相关:

  • 把变量声明推到作用域顶部:context.hoistVariableDeclaration(node)
  • 检查局部变量是否被引用
  • 定义唯一变量、重命名 binding 及其所有引用

查找:

  • 获取行列号:sourceFile.getLineAndCharacterOfPosition(node.pos)

高级:

  • 表达式求值
  • 跟随模块 import / node_modules import
  • 转换 jsx、判断与重置文件 pragma(/* @jsx */)

技巧

  • 组合多个 transformer:按顺序放进数组
  • 抛语法错误改善开发体验

测试

ts-transformer-testing-library。

已知 bug

  • EmitResolver 无法处理不是来自 parse tree 的 JsxOpeningLikeElement / JsxOpeningFragment
  • getMutableClone(node) 在配合 ts-loader 使用时爆炸

相关项目与工具

tags: TypeScript编译器, AST, Transformer, Babel

推荐文章

程序员茄子在线接单