BabelでTypeScriptメタプログラミング

これは、TypeScriptの型情報の取得をBabelを使って行うものという、とても誰得感漂う記事です。

## Babel vs TypeScript compiler

Babelは、ECMAScript/TypeScript/Flow などを扱う上では、汎用的でとても良いのですが、型の深淵に本気で突っ込むなら、TypeScript Compiler API を使う方が望ましいケースも多いでしょう。

技術書典7で、お隣のお隣のブースがTakepepeさんのところで、新刊 TypeScript Compiler API を軽く読みました。この本ではTypeScriptに特化したメタプログラミングについて書かれた本です。

* https://takepepe.booth.pm/

BabelでTypeScriptの型情報を取り扱うのは限界がありますが、この記事ではBabelでやっていくものです。利点はあってとても軽いことです。

Babelでは単に構文解析と型情報を消すstripや他を行うだけなので、TypeScript Compilerが本来やってることをほとんどやらないため、当然です。

また、Babelであれば、TypeScript(ECMAScript）以外も、同じ手順で取り扱える利点があります。今更Flowを触ることも無いでしょうが、Flowもほぼ同じ手順で操作できます。（ただし、ASTの型情報は大分違いがあります）

## AST を見てみよう

たとえば、

```ts
const code = `const createFileTodos: GatewayFactory<
  FileTodoPort,
  { files: FilesPort },
  { filename: string }
> = (ports, { filename }) => {}`
```

このような`GatewayFactory`型のアロー関数のソースを解析するとします。

```ts
export const viewAst = (ast: any) => {
  const replacer = (key: string, value: any) => {
    if (['loc', 'start', 'end'].includes(key)) {
      return undefined
    } else {
      return value
    }
  }
  console.log(JSON.stringify(ast, replacer, '  '))
}
```

BabelでパースしたASTには、`loc` `start` `end` など、構造を見たいだけのときには不要なプロパティが含まれていて、これがけっこう邪魔なので、JSON.stringify に、replacer関数を渡すことで少しスッキリさせています。

JSON.stringifyは3つの引数を受け取れます。1つめはJSON化する対象オブジェクトで、大半の人はこれだけを指定するでしょう。3つめは、表示する時のスペーサーです。半角スペース2つを指定しておけば大体ダイジョウブでしょう。`'. '` を指定すると少し表示がうるさくなりますが、インデントはわかりやすくなるかもしれません。

2番目の引数が今回の鍵である変換関数です。変換関数には2つの引数があり、JSONのキーと中身であるバリューです。これらを元に `undefined` か表示したい内容を返します。今回は、`loc` `start` `end` を消すだけなので、`['loc', 'start', 'end'].includes(key)` が真になる、つまり、この3のキーに該当する場合は `undefined` を返し、それ以外はそのまま `value` を素通しするだけです。

 ```ts
import { parse } from '@babel/parser'

const res = parse(code, { plugins: ['typescript'] })
viewAst(res.program.body[0])
```

`@babel/parser`の`parse`に`plugins: ['typescript']`を付けると、ASTが帰ってきます。別の方法としては `@babel/core` の `transformSync` で、AST を取得することもできます。

```json
{
  "type": "VariableDeclaration",
  "declarations": [
    {
      "type": "VariableDeclarator",
      "id": {
        "type": "Identifier",
        "name": "createFileTodos",
        "typeAnnotation": {
...
```

変数定義は、`VariableDeclaration` AST型のノードの中にある`declarations`メンバーが個々の定義 `VariableDeclarator` AST型です。

`VariableDeclarator` AST型の `id.type` は `Identifier` であり `name` が、変数名 `createFileTodos` になります。ここにTypeScriptの場合は、型アノテーション情報が追加されていて `typeAnnotation` キー以下に型アノテーション情報が入ります。

```json
        "typeAnnotation": {
          "type": "TSTypeAnnotation",
          "typeAnnotation": {
            "type": "TSTypeReference",
            "typeName": {
              "type": "Identifier",
              "name": "GatewayFactory"
            },
```

少し深めのツリーになるという欠点があります。

`typeAnnotation.typeAnnotation.typeName.name` までアクセスしないと、型名を取得できません。今回は `GatewayFactory` がそのTypeScriptの型名です。

`GatewayFactory`はジェネリクスでありそのための型引数を持ちます。

```json
"typeParameters": {
  "type": "TSTypeParameterInstantiation",
  "params": [
```

型引数は `typeParameters.params` 配列から取得できます。

```json
{
  "type": "TSTypeReference",
  "typeName": {
    "type": "Identifier",
    "name": "FileTodoPort"
  }
},
```

1つめの型引数は `FileTodoPort` です。

```json
{
  "type": "TSTypeLiteral",
  "members": [
    {
      "type": "TSPropertySignature",
      "key": {
        "type": "Identifier",
        "name": "files"
      },
      "computed": false,
      "typeAnnotation": {
        "type": "TSTypeAnnotation",
        "typeAnnotation": {
          "type": "TSTypeReference",
          "typeName": {
            "type": "Identifier",
            "name": "FilesPort"
          }
        }
      }
    }
  ]
},
```

2つめの引数はとてもややこしくて、`TSTypeLiteral` AST型です。`{files: FilesPort}`という型リテラルです。`members` 配列は、今回1つのメンバーしか無いため、配列長1です。

型リテラルの、左側 `files: ` は`TSPropertySignature`というAST型で、`key.name`で `files` を取得できます。右側 `FilesPort` TypeScript型は、これまでと同じ `TSTypeAnnotation` であり、`typeAnnotation.typeAnnotation.typeName.name` で `FilesPort`を取得できます。

```json
"typeAnnotation": {
  "type": "TSTypeAnnotation",
  "typeAnnotation": {
    "type": "TSStringKeyword"
  }
}
```

これは、3番目の引数に出てくるもので、右側がECMAScriptにも元々ある `string` であるため、`TSStringKeyword`という型になります。

結局のところ、BabelでTypeScriptの型定義を解析する場合、このように地道にどういうパスで取得可能なのか？を調べるだけの簡単なお仕事になります。

定番のastexplorerもいいですが、今回作った`viewAst`のように不要な情報をカットしたものの方が便利だったりすることもあります。

## モチベーション

なぜこんなことをしているかというと、筆者が最近作っている [@noxt/gateway](https://github.com/noxtjs/gateway) というプログラムでTypeScriptの解析をしてるためです。

* `GatewayFactory` 型で定義された関数を探し出す
* 型引数1つめを自分自身がファクトリメソッドで作り出すTypeScript型名の取得
　- その型がどこで定義されてるか `import` をたどって、型定義のソースコードを取得
* 2つめの引数には、ファクトリメソッドで初期化時に必要となる別の型名の取得
* 3つめには、設定として渡すべき項目の取得

などを行います。

ファクトリメソッドの定義と、それが要求する型情報の取得と、初期化に必要なデータを自動で揃えて、ファクトリメソッドを実行するというTypeScriptに特化したメタプログラミングです。


TypeScript Compiler API ならもっと簡単に作れたかもしれない……。


クリーンアーキテクチャの重要なアイデアの1つである ports and adaptersデザインパターンに合わせたDIをメタプログラミングで実現するというアプリです。