LoginSignup
10
9

More than 3 years have passed since last update.

ts-morphを使ってTypeScriptプロジェクトのリファクタ・AST操作をお手軽にやる

Last updated at Posted at 2019-10-13

ts-morphとは

https://github.com/dsherret/ts-morph
https://ts-morph.com/

元は ts-simple-ast という名前で、TypeScriptのASTを簡単に触れるようにしたラッパーで、ASTのナビゲーションやファイル操作、カスタムトランスフォーマーが簡単に使えるようなAPIを提供してくれている

どんなときに使うの?

  • 既存資産の大規模リファクタ用スクリプト
    • 機械的な置換だけじゃ対応しきれない、コンパイラが知ってる情報を使った変換をしたいときに利用出来る
    • ファイル内の特定の関数やinterfaceを別ファイルに分離したいとか
  • 開発補助ツールとして
    • カスタムlinterとかカスタムトランスフォーマーとかジェネレータとか
      • Angular CLIのgenerateみたく、ファイル生成+ファイル変更みたいなことがしたいときにファイル変更がやりやすくなる
    • @Takepepe さん作のvuexの型自動生成ツール みたいなの
      • (こちらはCompilerAPIを生で使っているが、こういうのを楽に作れる(はず

使い方

最低限のセットアップ

TypeScriptのプロジェクトを触るための初期セットアップコードはこれだけ

import { Project } from 'ts-morph';

const project = new Project({ tsConfigFilePath: path.join(process.cwd(), 'tsconfig.json') });

この project はTypeScriptプロジェクトに関する情報をよしなに作ってくれて、TypeScript自体のLanguageServiceやCompilerAPIへはこの projectを経由してアクセスが出来るようになっている

AST Navigation

AST Nodeの走査やファイル内のinterfaceのみを抽出するメソッド、コンパイルエラーの取得などの便利メソッドがある

import { Project } from 'ts-morph';

const project = new Project({ tsConfigFilePath: path.join(process.cwd(), 'tsconfig.json') });

const file = project.getSourceFile('path/to/file.ts');
const diagnostics = file.getPreEmitDiagnostics();  // コンパイルエラー一覧取得
const statements = file.getStatements(); // ファイル内の文を取得。トップレベルに定義されている定義などが列挙されるっぽい
const interfaces = file.getInterfaces(); // interfaceだけ取ってくるとかもできちゃう
file.forEachDescendant(node => myVisitors.forEach(v => v.visit(node)));  // 子孫Nodeを列挙してくれる。生CompilerAPIだと自前で再帰しないと末端Nodeまで辿れないけどこいつは全部出してくれる風味(ちゃんと調べてないので嘘かも

一度生CompilerAPIを触ったことある人なら↑のメソッドだけでも便利さが伝わると思う
一々Nodeを走査してDeclarationStatementか判定して・・・とかやって取ってきてた情報がメソッド一発なので超便利

ソースコードの変更

import { Project } from 'ts-morph';

async function main(){
  const project = new Project({ tsConfigFilePath: path.join(process.cwd(), 'tsconfig.json') });

  const file = project.getSourceFile('path/to/file.ts');
  file.move('path/to/newFile.ts');  // ファイル移動。もちろん参照してるファイルのimport文なんかも変更される
  file.organizeImports();  // import文の整理してくれるやつ
  file.getInterface().setIsExported(true)  // 任意のDeclarationにexportつけるためのメソッドまであって便利

  await file.save();  // fileへの変更の保存

  await project.save();  // project全体の変更の保存。操作したファイル以外に影響する変更が保存される
}

main().catch(console.error);

変更系も便利メソッドが豊富で非常に扱いやすい

コンパイル結果出力

import { Project } from 'ts-morph';

async function main(){
  const project = new Project({ tsConfigFilePath: path.join(process.cwd(), 'tsconfig.json') });
  await project.emit();  // project全体のコンパイル結果出力

  const file = project.getSourceFile('path/to/file.ts');
  await file.save();  // file単体の出力も可能っぽい

}

main().catch(console.error);

emit周りは全くいじってないのであんまわからない。。

おまけ

実際に自分が書いたスクリプト

このスクリプトは、 src/modules/**/*MT.ts ファイルに置かれていたinterface郡を interface.ts に分離して、import/exportを解決ということをしている

単純にinterface定義を別ファイルに移動するだけだとTypeScriptのリファクタリング機能ではimportの解決が出来ない(エディタで同様のことをやったときのことを想像してもらえればと)ので、
- interfaceを別ファイルへ移動
- コンパイルエラーが出ているimport文を削除
- fixMissingImportsを実行

という手順でファイルの変更を実施している(ちょっと強引)

なお、この fixMissingImports は同名の変数が複数あると適切なのを選ぶわけじゃなく、候補の一番上のものをimportしちゃうので、そこだけは手で直す必要があるのでちょい残念


というわけでts-morph便利なのでみんな使おう

10
9
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
10
9