31
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

TypeScript Transformerについてのお話

Last updated at Posted at 2017-09-15
1 / 21

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

31
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?