これは、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/parser
のparse
にplugins: ['typescript']
を付けると、ASTが帰ってきます。別の方法としては @babel/core
の transformSync
で、AST を取得することもできます。
{
"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
キー以下に型アノテーション情報が入ります。
"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.name
で files
を取得できます。右側 FilesPort
TypeScript型は、これまでと同じ TSTypeAnnotation
であり、typeAnnotation.typeAnnotation.typeName.name
で FilesPort
を取得できます。
"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をメタプログラミングで実現するというアプリです。