TS AST转换实例:从类型声明生成初始化数据
前言
如果在TS
中声明了一些对象类型,使用这些对象类型来注解变量时,这些变量值的初始化往往需要自己手动按对象结构填写一遍,尽管有了TS
类型声明和IDE
的智能提示的加持下,这个填写过程还是很透明快速的;但是仔细一想,明明在声明这些对象类型的时候已经填过一层结构,为啥在数据初始化的时候又不得不去再填一遍,难道就不能自动根据声明的结构和一些简单的规则直接生成我们想要的初始化数据吗?
这是可以做到的;答案就来自于TS
本身提供的编译器相关的API
,只要从编译的角度去看待上面的问题,那么解决方案还是“比较轻松”的;
实现流程
原理
从原理上来说,就是借助JSDoc
语法,根据AST
结构找到特定用于生成初始数据的注释,返回一份新的AST
结构,最后根据新的AST
生成输出的代码;
TS编译流程
TS
编译器核心相关的功能大致上有这么几个:
- 预处理器:加载源码文件及其依赖文件(通过
import
语句导入或者<refrence path=xxx />
标签语法导入的声明文件)和tsconfig.json
配置文件中额外加载的声明文件,并将这些文件加入到编译上下文中; - 解析器:对源码文件做出词法分析,并得到相应的
AST
(抽象语法树)结构; - 结合器:遍历
AST
,找到其中的具名声明结点,并将同一实体的不同声明进行归纳(这个实体通常是Identifier
,即标识符,也就是具名的)形成这个实体的Symbol
,同时Symbol
内也会记录对应声明的有效作用域; - 类型检查器:根据得到
Symbol
信息得出相应的Type
信息,以节点的Type
信息作为预期语义,以节点的AST
信息作为实际语义,对节点做出语义诊断,然后得出相应的诊断信息(如error
/warning
等); - 发射器:发射器根据编译选项以及
AST
信息(SourceFile
结点)输出相应的目标代码(包含js(x)/js.map/d.ts
);
相关概念
-
Symbol
:Symbol
是专门用于连接同一实体在AST
中所有的具名声明结点(相应地自然就有匿名声明结点,声明既包括一般语法的声明,也包括类型系统中的类型声明),因此是语义系统中的基本结构[1]; -
Type
:Type
则是语义系统中另一种基本结构,包括具名和匿名的类型;Type
是从Symbol
推断得到的最佳公共类型(best-common-type
),这里的类型可以是手动注解的,当没有手动注解时就会从值的内容进行推导; -
诊断信息(
semantic diagnostic
):即语义诊断信息,语义和语法是两种不同的系统,语义存在于类型系统中,是专门用于描述类型关系的,而诊断信息就是类型检查的产物;
TS AST数据结构
每一个待编译源码文件经过解析器的词法分析后,都会得到一个AST
,该树的顶点结点为SourceFile
,一般结点为Node
,而叶结点则是一些特殊的Node
结点,如下图所示:
Node
:AST
中最基本的结点,通常不是叶结点;一般包含pos
(在源码文件中的起始位置)、end
(在源码文件中的结束位置)和parent
(父结点)等属性,具体结构可以查看TS
源码中的Node interface
定义;SourceFile
:AST
中的顶点,本身也是一种Node
,还包含了一些源码文件相关的信息以及获取源码的方法;
TS编译常用API
注:以下均为ts namespace下的API;XXX意为某个Node interface的名称;
createSourceFile
:根据源码文本生成一个相应的AST
(即SourceFile
);createPrinter
:生成TS
代码打印器实例,可以根据AST
结点(包括SourceFile
)信息生成相应的TS
代码;transform
:根据提供的AST transformer
工厂函数,对AST
结点进行转换;isXXX
:快速判断某个AST
结点是否为某种特定的结点;factory.createXXX
:创建一个新的AST
结点;factory.updateXXX
:修改一个存在的AST
结点;之前可以通过直接修改结点属性来修改结点信息,现在已经不推荐这么做了(新版API
会逐渐把结点属性全部设置为只读);visitNode
:访问AST
结点,可以通过访问器函数返回值来替代或删除当前结点;visitEachChild
:用法同visitNode
类似,不过会向下遍历树的子节点;forEachChild
:用于遍历AST
结点的所有子结点,跟访问器不同,这里的返回值并不会影响遍历的结点;
上述所有的API
详情建议查看源码,或者直接在IDE
中查看代码提示(很详细,有的甚至给出了代码示例);factory
是TS4.0
新推出的工厂函数API
,用于替代之前散乱的结点修改API
,具体理由可以查看这个pull,从实践上来看新版的工厂函数API
用起来确实更加统一了,使用成本就低了,毕竟和结点的interface
一一对应了,老版API
还会存在工厂函数名称与interface
名称不一致的情况,写起来就有点坑;
实现细节(仅供参考)
本文中提到的需求并不需要通过创造新语法来实现,只是通过修改AST
结构即可实现,因此还算是“比较轻松”;
JSDoc基本语法
JSDoc
是一种扩展js
注释用法的插件(主要是生成文档和提示),不过现在在各种IDE
的广泛支持下,JSDoc
算是一种事实上的注释标准了,且可以承担一部分同TS
类似的类型注解功能;JSDoc
可以被编译器识别成正确语法,且TS
编译器解析时也会对JSDoc
语法解析并生成AST
结点;因此JSDoc
语法可以作为语法扩展的一个突破点,其大致语法如下:
1 | /** |
其本质上是利用了多行注释的部分,然后基于文本解析进行具体的细分:
comment
:注释文本部分;tagName
:注释标签;tag-comment
:注释标签说明文本;
事实上,JSDoc
根据功能特性已经内置了一些标签,且标签后面的文本部分也做了更具体的语法切分;不过作为基于JSDoc
语法的扩展,一般都是使用自定义的标签名称;
JSDoc结点
TS
中的JSDoc
结点并非是单独存在的,而是依附在JSDoc
注释后面的语法结点,可以通过该结点的jsDoc
属性来访问,不过需要注意的是目前jsDoc
属性属于API
的内部属性,因此结点接口声明文件上并未暴露;如:
1 | /** |
因此通过这些特性就可以定制一些自定义的JSDoc
标签和语法格式,然后通过AST
结点进行匹配,进而定制相应的AST
结点;
DSL设计
1 | /** |
@model
:model
标签用于标记该声明类型需要进行初始化,使用特定标记可以加快遍历速度;@default
:default
标签用于显式标记属性的默认值,没有指定的则使用默认规则;value
:用于指定默认值,可以是字面量,也可以是匿名函数,事实上其本质也可以是一段代码,取决于具体需求,可以解析成一个值就行ir;
1 | // 输出的目标初始化函数 |
AST修改流程
后话
修改语言特性一直都让人望而却步,好像编译器和AST
是有点遥不可及的东西;不过事实上接触了TS
的编译器API
后,发现其AST结构相当的透明,API
自由度也是很大的,且相当的规范统一;再加上各种用于辅助查看AST
结构的网站,其实修改AST
也不是那么的遥远和困难;所以,如果不满足语言特性的功能,可以大胆地尝试自己去扩展或者修改。
相关文档
- javascript - Generate object from Typescript interface - Stack Overflow:一种可能性
- google/intermock: Mocking library to create mock objects with fake data for TypeScript interfaces:利用ts类型生成mock数据的插件
- javascript - How to stub a Typescript-Interface / Type-definition? - Stack Overflow
- TypeStrong/ts-loader: TypeScript loader for webpack:
ts-loader
开放了transformer的相关配置 - Igorbek/typescript-plugin-styled-components: TypeScript transformer for improving the debugging experience of styled-components:一个使用了TS transformer API的插件
- Architectural Overview · microsoft/TypeScript Wiki:
TS
架构说明,主要是编译相关的架构 - Getting Started With Handling TypeScript ASTs:
AST
修改入门案例 - Refactor node factory API, use node factory in parser by rbuckton · Pull Request #35282 · microsoft/TypeScript:重构
TS AST
结点工厂函数的理由 - ts-ast-viewer:强大的
TS AST
结构查看工具,甚至给出了结点的工厂函数以及构造参数,有了它写AST
结构就方便多了 - Use JSDoc: Index
- TypeScript Language Specification
- 探索类型系统的底层 - 自己实现一个 TypeScript - SegmentFault 思否
- Typescript 编译过程 - 知乎