[TypeScript 2.4] custom transformer を利用して実行時に型情報を参照可能にする

# お詫び

当初、 TypeScript 2.3 の機能として紹介していましたが、正しくは 2.4 でした。
誤った情報を掲載してしまい、申し訳ありません。
TypeScript 2.3.1 では、 API として custom transformer を指定できるようにはなっているものの、 [bug fix](https://github.com/Microsoft/TypeScript/pull/15377) が取り込まれておらず、正常には使えない状態です。

# 以下、本文

現在の typescript@next では、コンパイル時に custom transformer を指定できるようになり、これによりコンパイラ機能の拡張が可能になりました。

参考: https://github.com/Microsoft/TypeScript/pull/13940

この記事では、この機能を使ってできることの例として、 string literal type の union のメンバーを列挙する関数について紹介します。

# 成果物
* github: https://github.com/kimamula/ts-transformer-enumerate
* npm: https://www.npmjs.com/package/ts-transformer-enumerate

# お題

以下のような signature の関数を実現します。

```ts
export declare function enumerate<T extends string>(): { [K in T]: K };
```

こんな風に使えます。

```ts
import { enumerate } from 'ts-transformer-enumerate';

type Colors = 'green' | 'yellow' | 'red';
const Colors = enumerate<Colors>();

console.log(Colors.green); // 'green'
console.log(Colors.yellow); // 'yellow'
console.log(Colors.red); // 'red'
```

これを実現するということは、 `type Colors` という TypeScript の型情報を、コンパイル時だけでなく、実行時にも参照できるようにする、ということです。
TypeScript に詳しい方ならご存知の通り、これは TypeScript のコンパイラをそのまま使う限り、おそらく将来にわたって不可能なことです。
なぜなら、 [TypeScript Design Goals](https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals) に、 "Non-goals" として以下のように明記されているからです。

> 5\. Add or rely on run-time type information in programs, or emit different code based on the results of the type system. Instead, encourage programming patterns that do not require run-time metadata.

# 種明かし

上記のコードを、作成した custom transformer を使ってコンパイルすると、以下のような JavaScript が出力されます。

```js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var ts_transformer_enumerate_1 = require("ts-transformer-enumerate");
var Colors = { green: "green", yellow: "yellow", red: "red" };
console.log(Colors.green); // 'green'
console.log(Colors.yellow); // 'yellow'
console.log(Colors.red); // 'red'
```

つまり、 `enumerate` 関数の呼び出しを、 custom transformer でいい感じに置換してやる、ということです。

# custom transformer の実装

詳細に説明するのが面倒なので、直接コードを載せます。
何となく流れを感じ取っていただければ。
AST をたどっていくイメージです。 (AST あまり詳しくないですが、たぶん)

```ts
import * as ts from 'typescript';
import * as path from 'path';

export default function transformer(program: ts.Program): ts.TransformerFactory<ts.SourceFile> {
  return (context: ts.TransformationContext) => (file: ts.SourceFile) => visitNodeAndChildren(file, program, context);
}

function visitNodeAndChildren(node: ts.SourceFile, program: ts.Program, context: ts.TransformationContext): ts.SourceFile;
function visitNodeAndChildren(node: ts.Node, program: ts.Program, context: ts.TransformationContext): ts.Node;
function visitNodeAndChildren(node: ts.Node, program: ts.Program, context: ts.TransformationContext): ts.Node {
  return ts.visitEachChild(visitNode(node, program), childNode => visitNodeAndChildren(childNode, program, context), context);
}

function visitNode(node: ts.Node, program: ts.Program): ts.Node {
  const typeChecker = program.getTypeChecker();
  // 置換対象の node かどうか (enumerate 関数の呼び出しかどうか) を判定
  if (!isEnumerateCallExpression(node, typeChecker)) {
    return node;
  }

  // enumerate 関数の呼び出しなら、 typeArguments から型情報を読み取る
  const stringLiteralTypes: string[] = [];
  node.typeArguments && resolveStringLiteralTypes(node.typeArguments[0], typeChecker, stringLiteralTypes);

  // 読み取った型情報をもとに、 object literal を生成して返すと、元の node が置換される
  return ts.createObjectLiteral(stringLiteralTypes.map(stringLiteralType => {
    // unquote string literal type
    const propertyName = stringLiteralType.substring(1, stringLiteralType.length - 1);
    return ts.createPropertyAssignment(propertyName, ts.createLiteral(propertyName));
  }));
}

function isEnumerateCallExpression(node: ts.Node, typeChecker: ts.TypeChecker): node is ts.CallExpression {
  if (node.kind !== ts.SyntaxKind.CallExpression) {
    return false;
  }
  const { declaration } = typeChecker.getResolvedSignature(node as ts.CallExpression);
  return !!declaration
    && (declaration.getSourceFile().fileName === path.resolve(__dirname, '..', 'index.ts'))
    && !!declaration.name
    && (declaration.name.getText() === 'enumerate');
}

function resolveStringLiteralTypes(node: ts.Node, typeChecker: ts.TypeChecker, stringLiteralTypes: string[]): void {
  switch (node.kind) {
    case ts.SyntaxKind.TypeReference:
      const symbol = typeChecker.getSymbolAtLocation((node as ts.TypeReferenceNode).typeName);
      symbol.declarations && symbol.declarations[0].forEachChild(node => resolveStringLiteralTypes(node, typeChecker, stringLiteralTypes));
      break;
    case ts.SyntaxKind.UnionType:
      node.forEachChild(node => resolveStringLiteralTypes(node, typeChecker, stringLiteralTypes));
      break;
    case ts.SyntaxKind.LiteralType:
      const text = (node as ts.LiteralTypeNode).getText();
      stringLiteralTypes.indexOf(text) < 0 && stringLiteralTypes.push(text);
      break;
    default:
      break;
  }
}
```

公開された API の使い方については現時点で公式のドキュメントがなく、私自身手探りで実装しましたが、きっとそのうち公開されると思うので、正確で詳細な情報については、その時にそちらをご参照いただけると良いと思います。
さわりだけ説明すると、ここでは node の種別（関数呼び出し、型参照等）の判定に `node.kind` を利用し、型の具体的な情報を知るために、 `ts.TypeChecker` を利用する、ということをやっています。

# custom transformer を利用したコンパイル

非常に残念なことに、 custom transformer を使うためには TypeScript のコンパイルを Node.js の API を利用して行わなければなりません。
以下のような感じです。

```js
const ts = require('typescript');
const enumerateTransformer = require('ts-transfomer-enumerate/transformer').default;

const program = ts.createProgram([/* your files to compile */], {
  strict: true,
  noEmitOnError: true,
  target: ts.ScriptTarget.ES5
});

const transformers = {
  before: [enumerateTransformer(program)],
  after: []
};
const { emitSkipped, diagnostics } = program.emit(undefined, undefined, undefined, false, transformers);

if (emitSkipped) {
  throw new Error(diagnostics.map(diagnostic => diagnostic.messageText).join('\n'));
}
```

当然ながら、 tsconfig.json とかで指定して普通に tsc で動くようにしてほしい、という要望が上がっており、今後に期待したいところです。
https://github.com/Microsoft/TypeScript/issues/14419

# 終わりに

実行時に型情報を参照するというのは、 TypeScript を長年（といっても３年くらいですが）愛用してきた身からすると、禁断の果実に手を出してしまう気分です（なので実際のところ濫用は避けたほうがいいかなと思っています）。
「実行時に型情報を参照する」という方向性でも、ここの例以外に色々な用途があるでしょうが、もちろんまったく異なる方向性にも、 custom transformer の使い道はあります。
https://github.com/Microsoft/TypeScript/pull/13940 に貼られている、他の project の issue, PR からのリンクを見ていると、どういうことに custom transformer が使われているか/使われようとしているかが分かって、なかなか興味深いです。
例えば、以下のようなものがあります。

* [tsickle](https://github.com/angular/tsickle) (TypeScript の code を Closure Compiler で利用可能なように変換するライブラリ) での利用
  * https://github.com/angular/tsickle/issues/285
* [tslint](https://github.com/palantir/tslint) の auto-fix 機能での利用
  * https://github.com/palantir/tslint/issues/2324

custom transformer が今後どのように使われていくか、それによって TypeScript にどのような変化が起きるか、期待していきたいです。