🗂

ts-morphを使ってテンプレートファイルを作成してみる

2024/12/24に公開

この記事は クラウドワークス グループ Advent Calendar 2024 シリーズ3の24日目の記事です。

株式会社ソニックムーブでバックエンドエンジニアをしています、福田です!
新卒で会社に入り3年目、最初はわからないことが多く混乱してましたが、最近はリーダー的な役割も任せてもらえるようになり楽しくエンジニアライフを送ってます(笑)

そんな自分の所属しているユニットでは主にAPIの作成業務が多いです。1日で1つのAPIを作る日もあるくらいの毎日です。
そしてこのAPIの作成業務ですが、標準化が結構進んでおり、基本的には同じようなファイルを作成するところからスタートします。毎回同じようなファイルを作成する日々に嫌気がさした自分はテンプレートのファイルを出力してくれる「ファイル自動生成ツール」を作成しました!
これによって、ケアレスミスが減って、コア部分の開発に注力ができるようになりました!我ながらとても良いものを作れたなと思ってます(自画自賛)

そんなバックグラウンドは置いておいて、上記の作業をするにあたり、結構便利だなと発見したツールがありました!それがts-morphです!
typescriptのコードをベースにファイルの内容を変更できるので、自動生成等にはうってつけのツールでした!

今回はそんな「ts-morph」を実際に利用して、ファイルを自動生成する部分について見ていこうと思います!実際にサンプルコードを見ながらそれぞれでどのようなことをやってるか解説していければと思います!

ts-morphの基礎

本題へ移る前に、ts-morphの基礎についてわかりやすい記事の紹介です。
ts-morph を使って大量の TypeScript コードを機械的に書き換えた
基本的な部分が簡潔にまとまっており、公式リファレンスよりわかりやすかったです!

ちなみにインストールは以下でできます。

npm install --save-dev ts-morph

開発でしか利用しないツールなのでdevDependenciesに入れたりましょう。

完成目標

今回は以下のことをやっていきたいと思います
・文字の置き換え
・他ファイルから該当情報の取得

そして各ファイルについて見ていきましょう。
以下がテンプレートファイルです。

import { /* functionName */Dto } from '@/usecases/dto//* functionName */Dto';

type ResponseJson = /* ResponseJsonList */

export class /* FunctionName */Response {
  public getJson(dto: /* FunctionName */Dto): ResponseJson {
    return /* DummyData */;
  }
}

interfaceを設定してるファイルは以下です。

export interface operations {
    response: {
        content: {
            id: number;
            name: string;
        }
    }
}

完成予定のファイルは以下です。

import { getNameDto } from '@/usecases/dto/getNameDto';

type ResponseJson = {
    id: number;
    name: string;
}

export class GetNameResponse {
  public getJson(dto: GetNameDto): ResponseJson {
    return {
        id: 1,
        name: "sample text"
    }
  }
}

現物コード

実際のコードは以下になります!

// 初期化
const project = new Project();
// ファイルの取得
const targetFile = project.addSourceFileAtPath(
    "path/to/editFile.ts"
);
const interfaceFile = project.addSourceFileAtPath(
    "path/to/interfaceFile.ts"
);

// インターフェイスを取得
const interface = interfaceFile.getInterfaceOrThrow("operations");

// interface内から欲しい情報を取得する
const responseInterface = interface
    .getPropertyOrThrow("response")
    .getTypeNodeOrThrow()
    .asKindOrThrow(SyntaxKind.TypeLiteral);
const contentInterface = responseInterface
    .getPropertyOrThrow("content")
    .getTypeNodeOrThrow()
    .asKindOrThrow(SyntaxKind.TypeLiteral);

// 取得したデータをダミーデータに変更する
const dummyData = generateDummyData(contentInterface.getType()); //ダミーデータを作成するオリジナル関数。今回は記事の意図とは異なるので省きます。
const dummyDataText = JSON.stringify(dummyData, null, 2);

// それぞれの値を置き換える
const targetFullText = targetFile.getFileText();
const replaceData = targetFullText
    .replace(/\/\*\sFunctionName\s\*\//g, GetName)
    .replace(/\/\*\sfunctionName\s\*\//g, getName)
    .replace('/* ResponseJsonList */', targetApplicationJson.getText())
    .replace('/* DummyData */', dummyDataText);

// 置き換えたテキストで再度設定をする
targetFile.replaceWithText(replaceData);

それぞれ1つずつ見ていきましょう!

Step.1 初期化

ts-morphを利用したい場合、以下のコードで初期化します。

const project = new Project();

初期化したあとはファイルの登録になります。このファイルへの登録は好きなだけファイルを指定できます!
今回は2種類も指定しちゃってます。欲張りです。

const targetFile = project.addSourceFileAtPath(
    "path/to/editFile.ts"
);
const interfaceFile = project.addSourceFileAtPath(
    "path/to/interfaceFile.ts"
);

Step.2 インターフェイスから該当データを取得する

インターフェイスの呼び出しには以下の処理を利用します

const interface = interfaceFile.getInterfaceOrThrow("operations");

「OrThrow」をつけることで、エラーを出力してくれるようになります。原因の探索に便利です!

次が肝の部分になります。

const responseInterface = interface
    .getPropertyOrThrow("response")
    .getTypeNodeOrThrow()
    .asKindOrThrow(SyntaxKind.TypeLiteral);
const contentInterface = responseInterface
    .getPropertyOrThrow("content")
    .getTypeNodeOrThrow()
    .asKindOrThrow(SyntaxKind.TypeLiteral);

上記でcontentプロパティのinterfaceの内容を取得することができます!
ですが、これだけ見せられても何をやってるのか結構わかりにくいです。なので、1行ずつ解説していきます。

getPropertyOrThrow

引数に設定したプロパティを探す関数です!今回はresponseプロパティを取得してます。
これによって、responseプロパティ内の値を簡単に出力させれたり、タイプを見たりすることができます!

const responseInterface = interface.getPropertyOrThrow("response")
responseInterface.getType();
responseInterface.getText();

ただ、getPropertyOrThrow("response").getPropertyOrThrow("content")のようなことをするとそのような関数は見つからないというエラーが出力されてしまいます。
なので、ここから正常に動くようさらに色々と関数を呼んであげて動かす必要があるんですね。大変。

getTypeNodeOrThrow

対応する TypeScript の構文ツリーのノード (TypeNode) を取得するために使用します。

asKindOrThrow(SyntaxKind.TypeLiteral)

ノード (Node) の種類を安全にチェックして、指定した SyntaxKind のノードとしてキャストしてます。
この場合は型リテラルかどうかを確認し、大丈夫な場合は型リテラルを返しています。

型リテラルにすることによって、再びgetPropertyを行うことができます!

ただ、これがちょっと長すぎる気がするので短くなってくれたら嬉しいなぁと思いますね。。。

Step.3 置き換え

該当する部分のタイプを取得できたら、いよいよ置き換えです。
この置き換えで利用できるのがreplaceメソッドです。

こいつを使って、該当の文字列を今回定義した文字列に置き換えてやります。

const repldaceData = targetFullText.
    .replace(/\/\*\sFunctionName\s\*\//g, GetName)
    .replace(/\/\*\sfunctionName\s\*\//g, getName)
    .replace('/* ResponseJsonList */', targetApplicationJson.getText())
    .replace('/* DummyData */', dummyDataText);

ついでに完成したテキストをそのままファイルに貼り付けることだってできます。

targetFile.replaceWithText(replaceData);

こんな感じでテンプレートファイルから作成予定のファイルに書き込みができます。

可能性の塊「ts-morph」

こんな感じでベースのファイルからテンプレートのファイルを作成することができました!
ts-morphを利用したら、他ファイルのtypescriptのコードを解析し、オリジナルのテンプレートファイルすら作成することも可能です。もしかしたらほぼほぼの作業を自動化できちゃうかも??
ただ課題としてts-morphはリファレンスが充実してないのを感じます。もっと進んでほしいですね。。。
実際にはかなり使えるサービスなので、標準化が進んで同じ作業をすることが多いと感じるようであれば、ts-morphを利用してどんどんファイルを出力するようにしちゃいましょう!

参考

ts-morph公式リファレンス
https://ts-morph.com/
ts-morph を使って大量の TypeScript コードを機械的に書き換えた
https://qiita.com/tomoasleep/items/d05e05ae14dce1b80d9d

株式会社ソニックムーブ

Discussion