LoginSignup
2
1

More than 3 years have passed since last update.

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

Posted at

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

Babel vs TypeScript compiler

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

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

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

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

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

AST を見てみよう

たとえば、

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

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

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 を素通しするだけです。

import { parse } from '@babel/parser'

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

@babel/parserparseplugins: ['typescript']を付けると、ASTが帰ってきます。別の方法としては @babel/coretransformSync で、AST を取得することもできます。

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

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

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

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

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

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

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

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

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

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

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

{
  "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.namefiles を取得できます。右側 FilesPort TypeScript型は、これまでと同じ TSTypeAnnotation であり、typeAnnotation.typeAnnotation.typeName.nameFilesPortを取得できます。

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

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

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

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

モチベーション

なぜこんなことをしているかというと、筆者が最近作っている @noxt/gateway というプログラムでTypeScriptの解析をしてるためです。

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

などを行います。

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

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

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

2
1
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
2
1