Sassyブログ

好きなことで暮らしを豊かにするブログ

TypeScript Compiler APIを触ってみた

始めに

今回はTypeScript Compiler APIというものについて触れようと思います。

私自身業務でTypeScriptを使っていますが、そこまで精通しているわけではないので間違っていることを話していましたらご指摘いただけると幸いです。

本記事で話す範囲

まずはTypeScript Compiler APIとは何か?

そしてこのAPIを使うことで何ができるのかを述べた後に実際にASTでのコード解析を行い、ASTから元のコードを復元できるかを試してみようと思います。

TypeScript Compiler APIとは何か?

以下のサイトから引用させていただきます。

TypeScriptを使用してアプリケーションを作成する場合、通常、「typescript」モジュールをビルドツールとして使用して、TypeScriptコードをJavaScriptに変換します。通常、必要なのはこれだけです。ただし、アプリケーションコードに「typescript」モジュールをインポートすると、コンパイラAPIにアクセスできるようになります。このコンパイラAPIは、TypeScriptコードと対話するための非常に強力なツールをいくつか提供します。

出典元:Compiler API (TypeScript) | learning-notes

ざっくり言ってしまうとTypeScriptコードをプログラム内でいじることができます。

このコンパイラの機能を使う上で重要になってくる要素として「AST」が挙げられます。

ASTとは抽象構文木のことでありプログラムの構造をツリーとして表現します。

TypeScript Compiler APIを使うと具体的に何ができるのか?

基本的にはコード解析とトランスパイル(ts→js)、コード生成(ASTから)がおこなえる認識です。

具体的にこのAPIを使うと、例えばWebAPIのインターフェース仕様書から型を自動生成することもできますし、linterを作ったりすることができるのではないでしょうか。

ASTによるコード解析を試してみる

では早速TypeScriptのコードを解析できるように環境を作っていきます。

今回はyarnで環境を構築していきますが、

以下のサイトを使えば簡単にASTを眺めることができます。

astexplorer.net

では以下のコマンドを叩いてTypeScriptを実行できる環境構築します。

作業フォルダを作成します。

$ mkdir testTscApi

yarnでプロジェクトを初期化します。

$ yarn init

typescriptをインストールします。

$ yarn add -D typescript

ts.configを作成します。

$ ./node_modules/.bin/tsc --init

環境構築完了後、適当に以下のファイルを作成しました。

testTscApi/main.ts

import * as ts from "typescript";
const program = ts.createProgram(["test.ts"], {})
const source = program.getSourceFile("test.ts")
if (source) {
    console.log(source.statements)
}

testTscApi/test.ts

import { testFunc2 } from "./test2";
export const testFunc1 = () => {
    console.log("test1");
    testFunc2()
}

testTscApi/test2.ts

export const testFunc2 = () => {
     console.log("test2");
}

上記のmain.tsを実行しますがjsへのトランスパイルが手間なのでts-nodeをインストールして直接typescriptを実行できるようにします。

$ yarn add -D ts-node

これでts-nodeコマンドを使ってmain.tsを実行します。

$ ./node_modules/.bin/ts-node main.ts

上記コードをASTに出力した結果がこちらです。

[
  NodeObject {
    pos: 0,
    end: 36,
    flags: 0,
    modifierFlagsCache: 0,
    transformFlags: 0,
    parent: undefined,
    kind: 262,
    decorators: undefined,
    modifiers: undefined,
    symbol: undefined,
    localSymbol: undefined,
    locals: undefined,
    nextContainer: undefined,
    importClause: NodeObject {
      pos: 6,
      end: 20,
      flags: 0,
      modifierFlagsCache: 0,
      transformFlags: 0,
      parent: undefined,
      kind: 263,
      isTypeOnly: false,
      name: undefined,
      namedBindings: [NodeObject]
    },
    moduleSpecifier: TokenObject {
      pos: 25,
      end: 35,
      flags: 0,
      modifierFlagsCache: 0,
      transformFlags: 0,
      parent: undefined,
      kind: 10,
      text: './test2',
      singleQuote: undefined,
      hasExtendedUnicodeEscape: false
    }
  },
  NodeObject {
    pos: 36,
    end: 111,
    flags: 0,
    modifierFlagsCache: 0,
    transformFlags: 2228736,
    parent: undefined,
    kind: 233,
    decorators: undefined,
    modifiers: [
      [TokenObject],
      pos: 36,
      end: 44,
      hasTrailingComma: false,
      transformFlags: 0
    ],
    symbol: undefined,
    localSymbol: undefined,
    locals: undefined,
    nextContainer: undefined,
    declarationList: NodeObject {
      pos: 44,
      end: 111,
      flags: 2,
      modifierFlagsCache: 0,
      transformFlags: 2228736,
      parent: undefined,
      kind: 251,
      declarations: [Array]
    }
  },
  pos: 0,
  end: 111,
  hasTrailingComma: false,
  transformFlags: 2228736
]

コードしては5行ほどですが、たくさん情報が詰まってますね…

statementsの配列の要素には意味ある単位に分割された解析情報が入っているようです。

今回のコードですと

以下の部分と

import { testFunc2 } from "./test2";

以下の部分の情報が要素に入っていますね。

export const testFunc1 = () => {
    console.log("test1");
    testFunc2()
}

ですね。

それでは軽くimport文のところだけを見てみましょう。

import文の構造は以下です。(上記の構造から抜粋しています)

NodeObject {
    pos: 0,
    end: 36,
    flags: 0,
    modifierFlagsCache: 0,
    transformFlags: 0,
    parent: undefined,
    kind: 262,
    decorators: undefined,
    modifiers: undefined,
    symbol: undefined,
    localSymbol: undefined,
    locals: undefined,
    nextContainer: undefined,
    importClause: NodeObject {
      pos: 6,
      end: 20,
      flags: 0,
      modifierFlagsCache: 0,
      transformFlags: 0,
      parent: undefined,
      kind: 263,
      isTypeOnly: false,
      name: undefined,
      namedBindings: [NodeObject]
    },
    moduleSpecifier: TokenObject {
      pos: 25,
      end: 35,
      flags: 0,
      modifierFlagsCache: 0,
      transformFlags: 0,
      parent: undefined,
      kind: 10,
      text: './test2',
      singleQuote: undefined,
      hasExtendedUnicodeEscape: false
    }
  },

importClauseというプロパティがあり、ネストして色々な情報が詰まっています。

importClauseに入っている情報は恐らく以下の部分のみの情報で

import { testFunc2 } 

moduleSpecifierプロパティには

from "./test2";

の部分の解析情報が入ってみたいです。

ASTから元のコードを生成してみる

以下のコードを書いて一度TypeScriptコードからASTを出力して、再度そのASTを元にTypeScriptコードを生成してみます。

import * as ts from "typescript";
const program = ts.createProgram(["test.ts"], {})
const source = program.getSourceFile(vtest.ts")
let code = ""
if (source) {
    console.log(source)
    const printer = ts.createPrinter()
    const sourceFile = ts.createSourceFile("output.ts", "", ts.ScriptTarget.Latest)
    code = printer.printNode(ts.EmitHint.Unspecified, source, sourceFile)
}
console.log(code)

出力結果です。

コンソールに以下の内容が出力されているかと思います。

import { testFunc2 } from "./test2”;
export const testFunc1 = () => {
    console.log("test1");
    testFunc2();
};

最後に

TypeScript Compiler APIを軽く触ってみただけですが、コード解析してASTを取得することで色々な情報を引き出せるので、これだけでもアイデア次第では色々と使い道がありそうだなと感じました。

そしてTypeScriptをインポートするだけで簡単に使えるので興味ある方は是非触ってみると面白いかと思います。