※ この資料はToKyoto.jsの発表資料として作成したものです
Transformer is 何
- 2.1で導入された機能
- TypeScriptのトランスパイル処理の本体
- async/awaitやgeneratorといった複雑なdownpileを見通しよく実装するための内部改善的な意味合いが強かった1
ちなみに、Transformer導入以前(<= v2.0.x)は、単一のファイル(emitter.ts)にトランスパイル処理が全て記述されていた2。
Transformer動作イメージ
例: Exponential Operator(ES2016)のdownpile
var square = x ** 2;
var square = Math.pow(x, 2);
TransformerはAST(抽象構文木)ベースの変換機構。
var square = x ** 2;
を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>
ノードへ置換する
<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表現を得る。
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
で動くよ。
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を作成することで、実現可能となった。
@deprecated("It will be removed in a future version.")
Class SomeBadClass {
// ...
}
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へと変換される。
export function add(a: number, b: number) {
return a + b;
}
/**
* @param {number} a
* @param {number} b
* @return {number}
*/
function add(a, b) {
return a + b;
}
exports.add = add;
Custom Transformersの苦悩
現時点で、Custom Transformersが流行ることはなさそう
主な理由:
- 再利用が難しい
- estreeとの互換性がない
- ドキュメント不足
理由1: 再利用が難しい
現状では、ts.Program#emit
や ts.transpileModule
を自分で叩かないとCustom Transformersを適用できない。
{
"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にもその気がない)
- 利用/自作には相応の覚悟をもつべし
参考リンク/脚注
参考:
-
https://github.com/Microsoft/TypeScript/blob/v2.0.10/src/compiler/emitter.ts から当時のemmitterが確認できる。9,000 locくらいあってヤバい。 ↩
-
https://github.com/longlho/ts-transform-css-modules にCSS Modules向けのtransformerがある ↩
-
https://github.com/alexeagle/ts_plugin_prototype/blob/master/eraseDecoratorsTransform.ts ↩
-
Googleあたりがclosure compilerやangular周りの何かをしたいから、政治力使った結果としてAPIに穴が空いたんじゃないかと邪推してる。特に裏付けはないけど。 ↩