TypeScript Compiler API の基本的な使い方、コード例と作ってみたもの

2月20日 (火) に JavaScript メタプログラミング勉強会 Metapro.es という勉強会があり、そこで TypeScript (TS) Compiler API について LT しました。内容は TS Compiler API の基本的な使い方を話したものですが、短い時間で話しきれる内容でなかったのと、TS Compiler API の日本語資料は少ないので、ここに補足記事を書いておきます。

また、example コードを Github に置いているので、適宜参照・実行しながら読むと理解しやすいかもしれません。

TypeScript Compiler API とは

その名の通り、TypeScript のコンパイラーをアレコレすることのできる API です。あまり実践的な使い方を話題に上げている人は見かけないですが、コードの解析、変換、型の取得など、色々と強力なことができます。例えば、TSLintは TS Compiler API を使ってコードの解析を行っていると聞きます。また、Angular も利用しているらしく、Transformer という機能を使っていたり、自分が前にコード読んだ時はテンプレート内の式の型チェックをするために、コンポーネントの型情報を利用していた覚えがあります。Vue のための VSCode 拡張の Vetur でも使われており、コードの補完やホバー時の情報表示に TypeScript の型定義の情報を利用しています。

ドキュメントはほとんど書かれておらず、使う際にはエディターの補完と型情報を頼りに手探りで書くことになります。また、Wiki にも記載のある通り、この API は安定版ではないので、これからのバージョンアップで壊れる可能性が十分にあります。ただし、破壊的変更はこのページで一覧にしてくれるようです。

コード例

簡単なコード例を挙げながら使い方を説明します。メタプロの勉強会で話した内容なので、メタプロっぽい機能だけ紹介します。TS Compiler API はフラットな名前空間で展開されているので、とりあえず import で読み込みましょう。

import * as ts from 'typescript'

ts 以下に、色々な関数、型などがあるので、とりあえず ts. まで入力して補完を眺めてみるだけでもそれっぽい関数が見つかるかもしれません。

コードの AST を取得する

この例は ts-compiler-api-examples の 1-read-ast.ts に対応しています。

まず、最も簡単な例として、すでにある TypeScript のコードの AST を取得してみましょう。この例ではまず Program というものを作ります。Program はコンパイル対象とするソースコードやコンパイルの設定をすべて保持しているオブジェクトで、TS Compiler API のエントリポイントです。Program を生成することで、関連するソースコードが内部的に AST に変換され、それを取り出すことができます。Program を生成するには ts.createProgram を実行します。

const program = ts.createProgram(['test.ts'], {})

ts.createProgram の第1引数には対象とする TypeScript のコードへのパス、第2引数にはコンパイラーオプションを渡します。

次に、test.ts の AST を取得するために Program から SourceFile を取得します。SourceFile はその名の通り、TypeScript のソースコードのファイルを表すオブジェクトで、パースされた AST が格納されています。SourceFile を得るためには program.getSourceFile に、ほしいソースコードのパスを渡して呼び出します。

const source = program.getSourceFile('test.ts')

SourceFile の AST に直接アクセスするには statements プロパティを参照すれば良いです。また、ts.forEachChild というヘルパー関数も用意されているのでそれを使うのも良いでしょう。AST の内部構造は型定義を見るのも良いですが、AST Explorer に対象とするコードを入力して確認するのが一番楽だと思います。

if (source) {
  // 直接 AST を参照
  console.log(source.statements)

  // ts.forEachChild を使って走査
  ts.forEachChild(source, node => {
    console.log(node)
  })
}

AST を構築し、コードを生成する

この例は ts-compiler-api-examples の 2-print-code.ts に対応しています。

AST を読むだけでなく生成することもできます。AST 生成の関数はすべて ts.create から始まるため、そこまで入力して補完からそれっぽいものを選びましょう。生成したい AST のノードの型 (kind) を AST Explorer で確認し、そのノードの名前を ts.create の後ろにつけると良いです。例えば、VariableDeclarationList のノードを生成したい時は ts.createVariableDeclarationList 関数を使います。

const ast = ts.createVariableDeclarationList([
  ts.createVariableDeclaration(
    ts.createIdentifier('test'),
    ts.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
    ts.createLiteral('Hello!')
  )
], ts.NodeFlags.Const)

AST を文字列として出力するには Printer を作ります。Printer は ts.createPrinter から作成することができます。

const printer = ts.createPrinter()

Printer に対して出力したいノードと適当な SourceFile を与えると文字列が出力されます。

const source = ts.createSourceFile('test.ts', '', ts.ScriptTarget.Latest)
const code = printer.printNode(ts.EmitHint.Unspecified, ast, source)
console.log(code) // const test: string = 'Hello!'; と出力される

型情報を取得する

この例は ts-compiler-api-examples の 4-read-type-info.ts に対応しています。

TS Compiler API を使ってコードの型情報を取得するには TypeChecker を利用します。TypeChecker はコードの型チェックを行うモジュールですが、型の情報を取得して利用することもできます。TypeChecker を得るには program.getTypeChecker を実行します。

const checker = program.getTypeChecker()

TypeChecker で得られるものには、主に型 (Type) とシンボル (Symbol) があります。型は普段 TypeScript を書いていて使う型と同じで、ある型を表す型です (紛らわしい)。シンボルに関しては理解が怪しいのですが、束縛を表すもので、それへの参照を解決するために生成されるオブジェクトだという理解です。例えば、以下のようにあるクラス Foo が宣言されている時、その下で new Foo() が呼び出されている時、この2つのノードはあるシンボルを通じて結ばれています。

class Foo {
  name = 'Foo'
}

const foo = new Foo()

以下はソースコード中のすべてのクラスから型情報を取得し、マークダウンとして出力する例です。型を取得するには checker.getTypeAtLocation、シンボルを取得するには checker.getSymbolAtLocation を使用します。どちらも引数にノードを渡し、そのノードに対応する型、および、シンボルが返されます。なぜかクラス定義のノードから直接型を取得するとインスタンス (new して得られるオブジェクト、new foo = new Foo()foo) の型が返り、クラス定義の Identifier のシンボルから型を取得するとコンストラクタ (new されるオブジェクト、new foo = new Foo()Foo) の型が返るので、以下の例ではそのどちらも取得しています。

ts.forEachChild(source, function next(node) {
  if (
    ts.isClassDeclaration(node) &&
    node.name
  ) {
    // クラスインスタンスの型を取得
    const type = checker.getTypeAtLocation(node)

    // クラスコンストラクタのシンボルを取得
    const ctorSymbol = checker.getSymbolAtLocation(node.name)
    if (!ctorSymbol) return

    console.log(printClassDoc(type, ctorSymbol))
  }
})

上記の printClassDoc の中身は以下のようになります。

function printClassDoc(type: ts.Type, ctorSymbol: ts.Symbol): string {
  // クラス名を取得
  let buf = '## ' + ctorSymbol.name + '\n'

  // コンストラクタの型を取得
  const ctorType = checker.getTypeOfSymbolAtLocation(ctorSymbol, ctorSymbol.valueDeclaration!)
  ctorType.getConstructSignatures().forEach(sig => {
    // 引数の型
    const params = sig.parameters.map(serializeSymbol)

    // 戻り型
    const ret = checker.typeToString(sig.getReturnType())

    buf += '\nnew (' + params.join(', ') + ') => ' + ret + '\n'
  })

  buf += '\n### Properties\n'

  // プロパティを取得
  type.getProperties().forEach(p => {
    buf += '\n- ' + serializeSymbol(p)
  })

  return buf + '\n'
}

function serializeSymbol(symbol: ts.Symbol): string {
  const type = checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration!)
  return symbol.name + ': ' + checker.typeToString(type)
}

コンストラクタの型 (new する時に通る引数と戻り型) を取得するために checker.getTypeOfSymbolAtLocation を使って、シンボルから型を取得しています。また、得られた型の getConstructSignatures を使って、new する時に関する情報を取得し、引数と戻り値の型情報を取り出しています。プロパティについても同じような感じです。

作ってみたもの

vuetype

確か一番最初に TS Compiler API を使って作ったのがこれで、Vue の SFC (.vue ファイル) から TypeScript 部分を抽出して .d.ts ファイルを出力するというものです。この記事では紹介していないのですが、LanguageService という API に .d.ts の文字列を出力する機能があり、それを使用しています。

typed-vue-template

Vue のテンプレートの型チェックをする試み第一弾で、クラス構文で書かれた Vue のコンポーネントにコンパイルされた render 関数を挿入してうまいこと型チェックさせようという試みでした。手探りで書いているのでかなりやっつけコードです。文字列で出力してしまってるので、エラーの位置がずれて微妙な感じだったので他の手段を探すことになりました。

vue-template-diagnostic

Vue のテンプレートの型チェックをする試み第二弾で、Angular の Language Service と同じで、自分で型チェッカーを書いてみたものです。コンポーネントの型情報を取得し、それをもとにテンプレート内の式を検証していく感じです。これは単純なケースだとうまく動いたのですが、複雑な型が入るかもしれないことを考えると (ジェネリックス、関数オーバーロードなど) つらくなってきたので諦めました。

Vetur のテンプレート型チェック機能

Vue のテンプレートの型チェックをする試み第三弾で、typed-vue-template と発想は似ていて、render 関数を TypeScript の型チェッカーにチェックさせるというアプローチです。しかし、こちらは HTML から生成した AST (HTML) を AST (TS) に変換し、それをそのまま型チェッカーに渡しています。これによって、エラーの場所はもとの HTML の適切な位置に出力されるし、TypeScript でチェックできるものはすべて型チェックできるようになりました。Vetur は VSCode のチームの人がオーナーだったり、TypeScript のコントリビューターが PR 投げてたりするので、結構 TS Compiler API の勉強になるかと思います。僕も Vetur のコードを読んで TS Compiler API の理解がだいぶ深まったと思います。

といったように、僕の場合は Vue のテンプレートの型チェックをしたくて TS Compiler API にいつの間にか詳しくなっていたという感じでした。

まとめ

TypeScript Compiler API でできることは結構あり、TypeScript で書かれたコードの解析、生成、型情報の取得など、様々なことができます。ただし、API はまだ安定しておらず、ドキュメントもほとんど書かれていないので、使う時には苦労すると思います。実例として、Vue のテンプレートの型チェックをする機能を作っていて、結構おもしろいことができます。

ただ、Babel 7 で TypeScript のコードをパースできるようになっているので、単純なコード解析や変換であれば Babel を使ったほうが良いんじゃないかとは思います。おそらく TS Compiler API じゃないとできないのは型まわりの情報を扱うことなので、そういうことをしたい時に使うのが良さそうです。