🪆

TypeScriptの変数から型定義を自動生成する

2024/08/05に公開

やりたいこと

明示的に型を指定していない変数の型定義を取得したい。エディタをホバーすると出てくる型定義をコピペするのを自動化するイメージ

const sample = [
  {id: 1, name: 'sample'}
];
type Sample = typeof sample // type Sampleの結果が欲しい

実行前後のコード

実行前は明示的な型定義がない配列

export const posts = [
  {
    userId: 1,
    id: 1,
    title:
      "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
    body: "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto",
  },
  {
    userId: 1,
    id: 2,
    title: "qui est esse",
    body: "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla",
  },
];

実行後は明示的に型定義がついている

export type Post = { userId: number; id: number; title: string; body: string };

export const posts: Post[] = [
  {
    userId: 1,
    id: 1,
    title:
      "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
    body: "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto",
  },
  {
    userId: 1,
    id: 2,
    title: "qui est esse",
    body: "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla",
  },
];

方法

ts-morphを使用する

処理の流れ

  1. ファイルにある変数を取得する(今回は1ファイルに1変数)
  2. 取得した変数が配列かどうか確認する(今回は配列)
  3. 1の変数から推論した型を作成
    4. 配列のままではなく、配列の中にあるオブジェクトを型とする
  4. ファイルの先頭に3で作成した型を挿入
  5. 1で取得した変数に3で作成した型をセット
  6. ファイルの保存

という処理を、指定したディレクトリ配下にあるファイル全てに行う
最後に変更を保存し、ついでフォーマットする

作成したスクリプト

import { exec } from "child_process";
import {
  OptionalKind,
  Project,
  SourceFile,
  TypeAliasDeclarationStructure,
  VariableDeclaration,
} from "ts-morph";

const project = new Project({
  tsConfigFilePath: "./tsconfig.json",
});
const sourceFiles = project.addSourceFilesAtPaths("src/**/*.ts");

const getTargetVariable = (sourceFile: SourceFile) => {
  return sourceFile.getVariableDeclarations()?.[0];
};

const createType = (
  targetVariable: VariableDeclaration,
  isTypeArray: boolean
) => {
  const elementType = isTypeArray
    ? targetVariable.getType().getArrayElementType()
    : targetVariable.getType();
  if (!elementType) {
    console.log("No element type found");
    return;
  }

  const typeAlias: OptionalKind<TypeAliasDeclarationStructure> = {
    name: `${targetVariable.getName()[0].toUpperCase()}${targetVariable
      .getName()
      .slice(1, -1)}`, // Start with uppercase and remove the last character
    type: elementType.getText(),
    isExported: true,
  };

  return typeAlias;
};

sourceFiles.forEach((sourceFile) => {
  const targetVariable = getTargetVariable(sourceFile);
  const isTypeArray = targetVariable.getType().isArray();
  const typeAlias = createType(targetVariable, isTypeArray);

  if (!typeAlias) {
    console.error("Type alias not created");
    return;
  }

  sourceFile.insertTypeAlias(0, typeAlias);
  targetVariable.setType(isTypeArray ? `${typeAlias.name}[]` : typeAlias.name);

  sourceFile.saveSync();
});

project.saveSync();
exec(`npx prettier --write src/**/*.ts`);

結果

エディタ(vscode)で変数をホバーしたときに出てくる型を書き込むことができました!

型が長い場合はコピペで取得するのが出来ないようなので、ファイル数が多くない場合でも使い所はありそうです

感想

ts-morphを使うと色々なことが出来そうです。
フロントエンドで使用するモックデータをMySQLからTypeScriptファイルに書き換えたときに、型定義も欲しいと思いやろうとしましたが、特に型定義は必要なかったので活躍の場はありませんでした。
他ににやりたかったことは、nestjsのcontrollerのレスポンスの型を推論して、dtoを生成することでしたが、取得したtypeを文字列以外で扱う方法がわからず挫折しました。

他にも

  • 特定の変数にJSDocを足す処理
  • ファイルを移動してインポートエラーが出た箇所の一括修正

といった様々な場面で使い始めています。

ドキュメントにもう一歩踏み込んだことが書いてあるとありがたいのですが、github copilotを活用しつつ、勘で書いては動かして試してを繰り返しながら少しずつ学んでいます。

検証で使用したリポジトリ

https://github.com/tatsuya-asami/create-type-definition-from-variable

sampleのデータを取得したサイト

https://jsonplaceholder.typicode.com/

コミューン株式会社

Discussion