💨

TypeScriptのコード分析を楽にする ts-morph入門

2024/12/19に公開

はじめに

この記事はTSKaigi Advent Calendar 2024 19日目の記事です。

https://qiita.com/advent-calendar/2024/tskaigi

1. ts-morphって何?

TypeScriptプロジェクトを扱っていると、こんな作業で困ったことはありませんか?

  • 「この関数をリネームしたいけど、使用箇所が多すぎて心配...」
  • 「新しいフィールドを追加したら、関連する型定義も全部直さないと」
  • 「外部APIのレスポンス型定義があるけど、バリデーション関数を手書きするのが大変」

このような作業を手動でやろうとすると、ミスのリスクが高く、膨大な時間がかかってしまいます。

ts-morphの役割

ts-morphは、TypeScriptのソースコードを扱うためのツールキットです。

https://ts-morph.com/

以下のような作業を安全に自動化できます:

  • コードの解析:クラスやインターフェース、関数の定義を探して、その内容を調べる
  • 型情報の取得:変数や関数の型を正確に把握する
  • コードの修正:メソッド名の変更や、新しいプロパティの追加を安全に行う
  • コードの生成:型定義を元に、新しいコードを自動生成する

なぜts-morphが必要か?

TypeScriptには公式のコンパイラAPIが存在します。しかし、この公式APIには大きな課題があります:

  1. 学習コストが高い:AST(抽象構文木)の詳細な知識が必要
  2. 冗長な記述:単純な操作でも多くのコードが必要
  3. エラーが起きやすい:型安全性が十分でない部分がある

ts-morphは、この公式APIをラップして、より使いやすいインターフェースを提供します。以下が、その違いの一例です:

// TypeScript Compiler APIの場合
const program = ts.createProgram(["./file.ts"], {});
const sourceFile = program.getSourceFile("./file.ts");
const classes = sourceFile?.statements.filter(
  (node): node is ts.ClassDeclaration => ts.isClassDeclaration(node)
);

// ts-morphの場合
const project = new Project();
const sourceFile = project.addSourceFileAtPath("./file.ts");
const classes = sourceFile.getClasses();

この記事では、ts-morphの基本的な使い方をなぞりながらどんなことができるのかを紹介します。

2. 始める前の準備

プロジェクトの初期化

ts-morphの使用を始めるには、まずインストールします:

npm install --save-dev ts-morph

# or

yarn add --dev ts-morph

# or

pnpm add --save-dev ts-morph

# or

bun add --dev ts-morph

実プロジェクトでの使用方法は主に3つのパターンがあります:

  1. 最もシンプルな初期化

    • 小規模なスクリプトや、一時的な使用に適している
    • 特別な設定は必要なし
  2. コンパイラオプションを指定

    • 特定のTypeScriptの機能に依存する場合に使用
    • たとえば、デコレータを使用する場合などに必要
  3. tsconfig.jsonを使用(推奨)

    • 既存のTypeScriptプロジェクトで使用する場合に最適
    • プロジェクトの設定と整合性が取れる

それぞれのパターンのコード例を見てみましょう:

import { Project } from "ts-morph";

// 1. シンプルな初期化
const project = new Project();

// 2. コンパイラオプションを指定
const project = new Project({
  compilerOptions: {
    target: ScriptTarget.ES2020,
    module: ModuleKind.ESNext,
    experimentalDecorators: true
  }
});

// 3. tsconfig.jsonを使用(推奨)
const project = new Project({
  tsConfigFilePath: "./tsconfig.json"
});

3. コード解析でできること

ts-morphの最も基本的な機能は、TypeScriptコードの解析です。単なる文字列としてではなく、型情報を含めた深い理解に基づいた解析が可能です。

基本的な解析例

たとえば、以下のようなTypeScriptコードがあるとします:

interface User {
  id: number;
  name: string;
  age?: number;
}

class UserService {
  private users: User[] = [];

  findById(id: number): User | undefined {
    return this.users.find(user => user.id === id);
  }
}

このコードについて、以下のような情報を簡単に取得できます:

  • インターフェースのプロパティ一覧
  • プロパティの型情報
  • オプショナルなプロパティの判定
  • メソッドの引数と戻り値の型
// インターフェースの解析
const userInterface = sourceFile.getInterfaceOrThrow("User");
const properties = userInterface.getProperties();

// プロパティの情報を取得
properties.forEach(prop => {
  console.log({
    name: prop.getName(), // プロパティ名
    type: prop.getType().getText(), // プロパティの型
    questionToken: prop.hasQuestionToken() // オプショナルかどうか
  });
});

// メソッドの解析
const userService = sourceFile.getClassOrThrow("UserService");
const findById = userService.getMethodOrThrow("findById");

このように、コードの構造を簡単に把握できます。

4. コード修正でできること

ts-morphの強力な機能の1つが、コードの安全な修正です。

安全な理由

ts-morphによる修正が安全な理由は以下の通りです:

  1. 型チェックを維持したまま修正できる
  2. 関連する箇所も自動的に更新される
  3. シンタックスエラーを防げる

代表的な修正操作

メソッド名の変更

// findByIdメソッドをgetByIdに変更
const method = userService.getMethodOrThrow("findById");
method.rename("getById");

この操作は以下のことを自動的に行います:

  • メソッドの定義の変更
  • そのメソッドを呼び出している全ての箇所の更新
  • インターフェースで定義されている場合、その定義も更新

インターフェースの修正

// 新しいプロパティの追加
userInterface.addProperty({
  name: "email",
  type: "string",
  hasQuestionToken: true  // オプショナルにする
});

5. コード生成の実践

ts-morphの実践的な使用例として、型定義からバリデーション関数を自動生成する機能を見てみましょう。

なぜバリデーションが必要か?

TypeScriptの型は、以下の理由で実行時の型チェックには使えません:

  1. 型情報はコンパイル時に消える
  2. APIレスポンスなど外部データの型安全性は保証されない
  3. ランタイムでの型チェックが必要

バリデーション関数の自動生成

以下のような型定義があるとします:

interface User {
  id: number;
  name: string;
  age?: number;
  email: string;
}

この型定義から、以下のようなバリデーション関数を自動生成できます:

function generateValidatorForInterface(interfaceDecl: InterfaceDeclaration) {
  const properties = interfaceDecl.getProperties();
  const functionName = `validate${interfaceDecl.getName()}`;

  const validations = properties.map(prop => {
    const name = prop.getName();
    const type = prop.getType();
    const isOptional = prop.hasQuestionToken();

    if (!isOptional) {
      return `if (data.${name} === undefined) return false;`;
    }

    // 型に応じたバリデーションを生成
    if (type.isString()) {
      return `if (data.${name} && typeof data.${name} !== "string") return false;`;
    }
    if (type.isNumber()) {
      return `if (data.${name} && typeof data.${name} !== "number") return false;`;
    }

    return '';
  });

  // バリデーション関数を生成
  return `
    function ${functionName}(data: unknown): data is User {
      if (!data || typeof data !== "object") return false;
      ${validations.join('\n')}
      return true;
    }
  `;
}

6. 便利な使い方のTips

エラー処理のベストプラクティス

ts-morphの多くのメソッドには2つのバージョンがあります:

  • getClass() - 失敗時にundefinedを返す
  • getClassOrThrow() - 失敗時に例外をスロー

使い分けの指針:

  • 存在が確実な場合は OrThrow バージョンを使用
  • 存在が不確実な場合は通常バージョンを使用

変更の保存

ts-morphでの変更は以下の手順で行うのがベストプラクティスです:

  1. 必要な変更をメモリ上で実行
  2. 全ての変更が成功したことを確認
  3. project.save() で一括保存

これにより:

  • パフォーマンスが向上(ファイルI/Oの最小化)
  • 途中でエラーが発生した場合も安全

このように、ts-morphを使うことで、TypeScriptコードの解析や修正を安全かつ効率的に行うことができます。型情報を活用した高度な操作が可能で、大規模なコードベースでも安心して使用できます。

7. まとめ

この記事では、ts-morphの基本的な使い方を紹介しました。TypeScriptのコードを安全に解析・修正するためのツールとして、ts-morphは非常に有用です。
私はts-morphを使って、先日1200ファイル以上のimport文を一括修正しました。単純な置換ではできないことだったので、手動でやると丸一日かけても終わらない作業を、ts-morphを使うことで数時間で完了させることができました。
ぜひ、あなたもts-morphを使って、TypeScriptのコードベースを効率的に管理してみてください!

TSKaigiについて

2025年5月23日(金)/24日(土)にTSKaigi 2025が開催されます!(なんと今回は2days開催!)
TSKaigiは日本最大級のTypeScriptをテーマとした技術カンファレンスです(前回の参加者2000人以上)
TypeScriptに興味のある方は、ぜひ公式サイトやXを確認してみてください!
私は運営として参加しているので、ぜひ会場でお会いしましょう!
現在スポンサー募集が開始しております!

https://2025.tskaigi.org/

GitHubで編集を提案
GMOメディアテックブログ

Discussion