TypeScript Transformerについてのお話

  • 16
    Like
  • 0
    Comment

※ この資料は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に穴が空いたんじゃないかと邪推してる。特に裏付けはないけど。