TypeScript Transformerについてのお話

※ この資料は[ToKyoto.js](https://kyotojs.connpass.com/event/64310/)の発表資料として作成したものです

---

### Transformer is 何

- 2.1で導入された機能
- TypeScriptのトランスパイル処理の本体
- async/awaitやgeneratorといった複雑なdownpileを見通しよく実装するための内部改善的な意味合いが強かった[^ts_transfomer_issue]

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

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

---
### Transformer動作イメージ

例: Exponential Operator(ES2016)のdownpile

```typescript:before.ts
var square = x ** 2;
```

```typescript:after.js
var square = Math.pow(x, 2);
```

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


```typescript:before
var square = x ** 2;
```

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

```xml: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>`ノードへ置換する

```xml: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表現を得る。

```typescript: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で導入された[^ts_exporse_transformer_pr]
- 独自の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.

[^ts_exporse_transformer_pr]: https://github.com/Microsoft/TypeScript/pull/13940
[^ts_transpile_comment]: https://github.com/Microsoft/TypeScript/pull/13940#issuecomment-304939084

---

### 実装例

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

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

```ts: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 Modules[^css_mod_transformer], Relay modern, etc...

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

---
### Ambient Decorator

Ambient(Design-time-only) Decoratorとは、「コードの実行時には一切の影響を及ぼさないDecorator」のこと。
(https://github.com/Microsoft/TypeScript/issues/2900 で議論されているけど、意見がまとまらなかったために未だ実装されてない)

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

```ts:source.ts
@deprecated("It will be removed in a future version.")
Class SomeBadClass {
  // ...
}
```

```ts:after
Class SomeBadClass {
  // ...
}
```

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

[^remove_decorators]: https://github.com/alexeagle/ts_plugin_prototype/blob/master/eraseDecoratorsTransform.ts
[^ts_ambient_descorators_issue]: https://github.com/Microsoft/TypeScript/issues/2900

---

## 使い所2

TypeScriptではsourceに付与されたtype annotationはすべて削り取られるため、.jsの側からそれらを参照するこはできなかった。
strip自体もts組み込みのtransformerで実行されているが、before transformerを利用すると型情報を.jsへ持ち出すことが可能となる。

---
### Closure Compiler

例: [tsickle](https://github.com/angular/tsickle)

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

```ts:source.ts
export function add(a: number, b: number) {
  return a + b;
}
```

```js: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#emit` や `ts.transpileModule` を自分で叩かないとCustom Transformersを適用できない。


```json:tsconfg.json
{
  "compilerOptions": {
    "transformers": {
      "before": ["my-transformer-factory"]
    }
  }
}
```

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

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

[^plugin_support_issue]: https://github.com/Microsoft/TypeScript/issues/14419

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

| package | 種別 | 対応状況 |
| :-- | :-- | :-- |
| gulp-typescript | gulp plugin | 未対応(issueあり[^gulp_plugin_issue]) |
| ts-loader | webpack loader | 対応済 |
| awesome-typescript-loader | webpack loader | 対応済 |
| fuse-box | module bundler | 未対応 |
| ts-node | Node.js require hook | 未対応(issueあり[^ts_node_issue]) |

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

[^gulp_plugin_issue]: https://github.com/ivogabe/gulp-typescript/issues/502
[^ts_node_issue]: https://github.com/TypeStrong/ts-node/issues/296

---
### 理由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として追加されたため[^babylon_ts_issue]、
.tsを扱うbabel pluginが作れるようになり、TS ASTは分が悪くなる一方。

[^babylon_ts_issue]: https://github.com/babel/babylon/issues/320

---
### 理由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側にも「どんどん使ってね！」という気持ちは無いようにみえる[^jasui]。

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

---
## まとめ

- Custom Transformersを利用すると、tsのAST変換に侵襲できる
  - 型情報を実行時まで引きまわすことも可能
- (caveat)
  - エコシステムとして発展する気がしない(おそらくMSにもその気がない)
  - 利用/自作には相応の覚悟をもつべし

---
## 参考リンク/脚注

参考:

- [custom transformer を利用して実行時に型情報を参照可能にする](http://qiita.com/kimamula/items/010d1fa8fd9a9d9ca207)