本文转载自微信公众号「神说要有光zxg」,作者神光的编程秘籍。转载本文请联系神说要有光zxg公众号。
现在 JS 的很多库都用 typescript 写了,面试也几乎必问 typescript,可能你对 ts 的各种语法和内置高级类型都挺熟悉了,对 ts 的配置、命令行的使用也没啥问题,但总感觉对 ts 的理解没那么深,苦于没有很好的继续提升的方式。这时候我推荐你研究下 typescript compiler api。
typescript 会把 ts 源码 parse 成 AST,然后对 AST 进行各种转换,之后生成 js 代码,在这个过程中会对 AST 进行类型检查。typescript 把这整个流程封装到了 tsc 的命令行工具里,平时我们一般也是通过 tsc 来编译 ts 代码和进行类型检查的。
但其实 ts 除了提供 tsc 的命令行工具外,也暴露了很多 api,同时也能自定义 transformer。这就像 babel 可以编译 esnext、ts 语法到 js,可以写 babel 插件来转换代码,也暴露了各种 api 一样。只不过 typescript transformer 的生态远远比不上 babel 插件,知道的人也比较少。
其实 typescript transformer 能做到一些 babel 插件做不到的事情:
- babel 是从 ts、exnext 等转 js,生成的 js 代码里会丢失类型信息,不能生成 ts 代码。
- babel 只是转换 ts 代码,并不会进行类型检查。
这两个 babel 插件做不到的事情,通过 typescript transformer 都可以做到。
而且,学会 typescript compiler 的 api 能够帮助你深入 typescript 的编译流程,更好的掌握 typescript。
说了这么多,我们通过一个例子来入门下 typescript transformer 吧。
案例描述
这样一段 ts 代码:
- type IsString<T> = T extends string ? 'Yes' : 'No';
- type res = IsString<true>;
- type res2 = IsString<'aaa'>;
我们希望能把 res 和 res2 的类型的值算出来,通过注释加在后面。
像这样:
- type IsString<T> = T extends string ? 'Yes' : 'No';
- type res = IsString<true> //No;
- type res2 = IsString<'aaa'> //Yes;
这个案例既用到了 transformer api,又用到了类型检查的 api。
下面我们来分析下思路:
思路分析
我们首先要把 ts 代码 parse 成 AST,然后通过 AST 找到要转换的节点,这里是 TypeReference 节点。
可以用 astexplorer.net 看一下:
IsString 是一个 TypeReference,也就是引用了别的类型,然后有 typeName 是 IsString 和类型参数 typeArguments,这里的类型参数就是 true。
是不是很像一个函数调用,这就是高级类型的本质,通过把类型参数传到引用的高级类型里求出最终的类型。
然后我们找到 TypeReference 的节点之后就可以通过 type checker 的 api 来求出类型值,之后创建一个注释节点添加到后面就行了。
转换完 AST,再把它打印成 ts 代码字符串。
思路就是这样,接下来我们具体来实现下,也熟悉下 ts 的 api。
代码实现
parse 代码成 AST 需要先指定要编译的文件和编译参数(createProgram 的 api),然后就可以拿到不同文件的 AST 了(getSourceFile 的 api)。
- const ts = require("typescript");
- const filename = "./input.ts";
- const program = ts.createProgram([filename], {}); // 第二个参数是 compiler options,就是配置文件里的那些
- const sourceFile = program.getSourceFile(filename);
这里的 sourceFile 就是 AST 的根结点。
接下来我们要对 AST 进行转换,使用 transform 的 api:
- const { transformed } = ts.transform(sourceFile, [
- function (context) {
- return function (node) {
- return ts.visitNode(node, visit);
- function visit(node) {
- if (ts.isTypeReferenceNode(node)) {
- // ...
- }
- return ts.visitEachChild(node, visit, context)
- }
- };
- }
- ]);
transform 要传入遍历的 AST 以及 transfomerFactory。
- AST 就是上面 parse 出的 sourceFile。
- transformerFactory 可以拿到 context 中的很多 api 来用,它的返回值就是转换函数 transformer。
transformer 参数是 node,返回值是修改后的 node。
要修改 node 就要遍历 node,使用 visit api 和 vistEachChild 的 api,过程中根据类型过滤出 TypeReference 的节点。
之后对 TypeReference 节点做如下转换:
- if (ts.isTypeReferenceNode(node)) {
- const type = typeChecker.getTypeFromTypeNode(node);
- if (type.value){
- ts.addSyntheticTrailingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, type.value);
- }
- }
也就是通过 typeCheker 来拿到 IsString 这个类型的最终类型值,然后通过 addSyntheticTrailingComment 的 api 在后面加一个注释。
其中用到的 typeChecker 是通过 getTypeChecker 的 api 拿到的:
- const typeChecker = program.getTypeChecker();
这样就完成了我们的转换 ts AST 的目的。
然后通过 printer 把 AST 打印成 ts 代码。
- const printer =ts.createPrinter();
- const code = printer.printNode(false, transformed[0], transformed[0]);
- console.log(code);
这样就可以了,我们来测试下。
测试之前,全部代码放这里了:
- const ts = require("typescript");
- const filename = "./input.ts";
- const program = ts.createProgram([filename], {}); // 第二个参数是 compiler options,就是配置文件里的那些
- const sourceFile = program.getSourceFile(filename);
- const typeChecker = program.getTypeChecker();
- const { transformed } = ts.transform(sourceFile, [
- function (context) {
- return function (node) {
- return ts.visitNode(node, visit);
- function visit(node) {
- if (ts.isTypeReferenceNode(node)) {
- const type = typeChecker.getTypeFromTypeNode(node);
- if (type.value){
- ts.addSyntheticTrailingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, type.value);
- }
- }
- return ts.visitEachChild(node, visit, context)
- }
- };
- }
- ]);
- const printer =ts.createPrinter();
- const code = printer.printNode(false, transformed[0], transformed[0]);
- console.log(code);
测试效果
经测试,我们达到了求出类型添加到后面的注释里的目的
复盘
激不激动,这是我们第一个 ts transformer 的例子,虽然功能比较简单,但是我们也学会了如何对 ts 代码做 parse、 transform,print,以及 type check。
其实 babel 也有 parse、transform、generate 这 3 步,但没有 type check 的过程,也不能打印成 ts 代码。
用 compiler api 的过程中你会发现原来高级类型就是一个 typeReference,需要传入 typeArguments 来求值的,从而对高级类型的理解更深了。
总结
对 typescript 语法和配置比较熟悉后,想更进一步的话,可以学习下 compiler 的 api 来深入 ts 的编译流程。它包括 transfomer、type checker 等 api,可以达到像 babel 插件一样的转换 ts 代码的目的,而且还能做类型检查。
我们通过一个例子来熟悉了下 typescript 的编译流程和 transformer 的写法。
当你需要修改 ts 代码然后生成 ts 代码的时候,babel 是做不到的,它只能生成 js 代码,这时候可以考虑下 typescript 的自定义 transformer。
而且用 typescript compiler api 能够加深你对 ts 编译流程和类型检查的理解。
ts compiler api 尤其是其中的自定义 transformer 是 typescript 更进一层的不错的方向。