Edited at

TypeScript Transformerについてのお話

More than 1 year has passed since last update.

※ この資料はToKyoto.jsの発表資料として作成したものです



Transformer is 何


  • 2.1で導入された機能

  • TypeScriptのトランスパイル処理の本体

  • async/awaitやgeneratorといった複雑なdownpileを見通しよく実装するための内部改善的な意味合いが強かった1

ちなみに、Transformer導入以前(<= v2.0.x)は、単一のファイル(emitter.ts)にトランスパイル処理が全て記述されていた2



Transformer動作イメージ

例: Exponential Operator(ES2016)のdownpile


before.ts

var square = x ** 2;



after.js

var square = Math.pow(x, 2);



TransformerはAST(抽象構文木)ベースの変換機構。


before

var square = x ** 2;


をASTで表現すると、以下のようになる


before.ast

<variable-statement>

<variable-declaration-list>
<identifier escaped-text="square" />
<binary-expression>
<identifier escaped-text="x" />
<asterisk-asterisk-token />
<first-literal-token text="2" />
</binary-expression>
</variable-declaration-list>
</variable-statement>


<binary-expression> ノードを、下記のように<call-expression>ノードへ置換する


after.ast

<variable-statement>

<variable-declaration-list>
<identifier escaped-text="square" />
<call-expression>
<property-access-expression>
<identifier escaped-text="Math" />
<identifier escaped-text="pow" />
</property-access-expression>
<identifier escaped-text="x" />
<first-literal-token text="2" />
</call-expression>
</variable-declaration-list>
</variable-statement>

このステートメント全体をtext表現に戻すと、元のソースのES2015表現を得る。


after.ts

var square = Math.pow(x, 2);



要するにBabel pluginのTypeScript版。

https://github.com/Microsoft/TypeScript/tree/master/src/compiler/transformers を覗いてみるとわかるが、


  • ES.next用

  • ES2017用(ES2017 -> ES2016)

  • ES2016用(ES2016 -> ES2015)

  • ES2015用(ES2015 -> ES5)

  • ES5用(ES5 -> ES3)

  • generator用

  • jsx用(なんでこいつがここにいるのやら...)

  • TypeScript本体用(type annotation, namespaceなどの独自構文の除去)

  • etc...

といったTransformerが存在している。

Babelと比べると1つ1つの粒度がデカいのが特徴。Babelにおけるpreset = TypeScriptにおけるTransformer くらい。

コンパイラオプション(target, module, jsx)の単位で制御できれば十分なため(Transformerの単位 = source ASTのvisit単位なので、ある程度まとめておいた方が高速)。



Custom Transformer


  • v2.4.1で導入された3

  • 独自のTransformerをTS組み込みのTransformerの前後に差し込めるようになった

  • 拡張できるとこ:


    • トランスパイル挙動



  • 拡張できないとこ:


    • Type Check

    • Module Resolution




Transforms, all of them, happen after the checking phase has happened. you can not add new files at this point.




実装例

以下は console.log(...)sayコマンド呼び出しに置換する例。

コンソールから $ ts-node main.ts | node で動くよ。


main.ts

import * as ts from "typescript";

const input = `
console.log("Hello, world.");
`
;

function transformerFactory(ctx: ts.TransformationContext) {
function visitNode(node: ts.Node): ts.Node {
if (!isConsoleLog(node)) {
return ts.visitEachChild(node, visitNode, ctx);
}
return createHelper(node);
}

function createHelper(node: ts.Node) {
ctx.requestEmitHelper({
name: "ts:say",
priority: 0,
scoped: false,
text: "var __say__ = function(msg) { require('child_process').execSync('say ' + msg); };",
});
return ts.setTextRange(ts.createIdentifier("__say__" ), node);
}

function isConsoleLog(node: ts.Node): node is ts.PropertyAccessExpression {
return node.kind === ts.SyntaxKind.PropertyAccessExpression && node.getText() === "console.log";
}

return (source: ts.SourceFile) => ts.updateSourceFileNode(source, ts.visitNodes(source.statements, visitNode));
}

const out = ts.transpileModule(input, { transformers: { before: [transformerFactory], } });
console.log(out.outputText);




いつ使うのか?



使い所1

webpackのloader的な処理をCustom Transformerで置き換える。

loaderは (source: string) => string な変換処理であるため、Transformerで処理した方が性能のアドバンテージがある。

e.g. unassert, CSS Modules4, Relay modern, etc...



Ambient Decorator

Ambient(Design-time-only) Decoratorとは、「コードの実行時には一切の影響を及ぼさないDecorator」のこと。

(https://github.com/Microsoft/TypeScript/issues/2900 で議論されているけど、意見がまとまらなかったために未だ実装されてない)

特定のDecoratorsを構文木から削除するTransfomer5を作成することで、実現可能となった。


source.ts

@deprecated("It will be removed in a future version.")

Class SomeBadClass {
// ...
}


after

Class SomeBadClass {

// ...
}

この使い方の最たる例はAngularのOffline Compiler(@Component({...}) 等が削除される)。



使い所2

TypeScriptではsourceに付与されたtype annotationはすべて削り取られるため、.jsの側からそれらを参照するこはできなかった。

strip自体もts組み込みのtransformerで実行されているが、before transformerを利用すると型情報を.jsへ持ち出すことが可能となる。



Closure Compiler

例: tsickle

TypeScriptから、Google Closure CompilerフレンドリーなJavaScriptへ変換する。型情報はClosure用のJSDocへと変換される。


source.ts

export function add(a: number, b: number) {

return a + b;
}


compiled

/**

* @param {number} a
* @param {number} b
* @return {number}
*/

function add(a, b) {
return a + b;
}
exports.add = add;



Custom Transformersの苦悩

現時点で、Custom Transformersが流行ることはなさそう :thinking:

主な理由:


  • 再利用が難しい

  • estreeとの互換性がない

  • ドキュメント不足



理由1: 再利用が難しい

現状では、ts.Program#emitts.transpileModule を自分で叩かないとCustom Transformersを適用できない。


tsconfg.json

{

"compilerOptions": {
"transformers": {
"before": ["my-transformer-factory"]
}
}
}

.babelrcよろしくtsconfg.jsonへキーを追加するissueも上がったのだけど6、mhegazyがやらないと明言している。


We do not plan on exposing compiler plugin system in the short term.



3rd party製のTypeScriptツール群ではCustom Transformersのサポートも少しずつ進んでいる

package
種別
対応状況

gulp-typescript
gulp plugin
未対応(issueあり7)

ts-loader
webpack loader
対応済

awesome-typescript-loader
webpack loader
対応済

fuse-box
module bundler
未対応

ts-node
Node.js require hook
未対応(issueあり8)

しかしながら、「Transformerでないとできない変換」要件がそれほど存在しないため(tsickleが相当特殊)、わざわざTransformerが作られるケースは少ないのでは。



理由2: estreeとの互換性がない

Custom TransformersはASTを相手にするわけだけど、このASTはTypeScript独自仕様。

一方、JavaScriptでASTをゴニョゴニョするといえば、大概estree準拠のAST.

Babel, eslint, prettier, webpack, etc...

estreeをデファクトとしてエコシステムが形成されている中、わざわざts独自ASTを食うツールを作るのにはそれなりのモチベーションが必要。

これはCustom Transformersだけの話ではなく、Language Service Pluginにも言えること。

TS AST -> estree, estree -> TS AST のconverter 作って公開したら需要あるかも?

しかもBabylonにTypeScript sourceのparserがpluginとして追加されたため9

.tsを扱うbabel pluginが作れるようになり、TS ASTは分が悪くなる一方。



理由3: ドキュメント不足


we are in the process of writing documentation and samples for these.


https://github.com/Microsoft/TypeScript/issues/14419#issuecomment-283721389

といったのは2017年の4月だけど、まったくドキュメントが整備される様子もない。

現状、TypeScript本家の https://github.com/Microsoft/TypeScript/tree/master/src/compiler/transformers 配下を参考にして手探りで作っていくしかない。

結局のところ、MS側にも「どんどん使ってね!」という気持ちは無いようにみえる10



まとめ


  • Custom Transformersを利用すると、tsのAST変換に侵襲できる


    • 型情報を実行時まで引きまわすことも可能



  • (caveat)


    • エコシステムとして発展する気がしない(おそらくMSにもその気がない)

    • 利用/自作には相応の覚悟をもつべし





参考リンク/脚注

参考:





  1. https://github.com/Microsoft/TypeScript/issues/5595 



  2. https://github.com/Microsoft/TypeScript/blob/v2.0.10/src/compiler/emitter.ts から当時のemmitterが確認できる。9,000 locくらいあってヤバい。 



  3. https://github.com/Microsoft/TypeScript/pull/13940 



  4. https://github.com/longlho/ts-transform-css-modules にCSS Modules向けのtransformerがある 



  5. https://github.com/alexeagle/ts_plugin_prototype/blob/master/eraseDecoratorsTransform.ts 



  6. https://github.com/Microsoft/TypeScript/issues/14419 



  7. https://github.com/ivogabe/gulp-typescript/issues/502 



  8. https://github.com/TypeStrong/ts-node/issues/296 



  9. https://github.com/babel/babylon/issues/320 



  10. Googleあたりがclosure compilerやangular周りの何かをしたいから、政治力使った結果としてAPIに穴が空いたんじゃないかと邪推してる。特に裏付けはないけど。