2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

モノレポでSequelizeモデルの型定義を自動生成・共有する方法

Posted at

はじめに

TypeScriptを使用したモノレポ構成のプロジェクトでは、バックエンドのSequelizeモデルの型定義をフロントエンドと共有することで、型安全性を確保できます。この記事では、モデルの型定義を自動生成し、コミット時に自動更新する方法を解説します。

プロジェクト構成例

.
├── frontend/
│   └── src/
│       └── types/
│           └── models.ts  # 生成される型定義ファイル
└── backend/
    └── src/
        ├── scripts/
        │   └── generateTypes.ts  # 型定義生成スクリプト
        └── models/
            ├── index.ts   # Sequelize設定
            ├── User.ts    # ユーザーモデル
            ├── Prototype.ts
            ├── Part.ts
            └── Player.ts

型定義生成スクリプト

1. 型変換ロジック(generateTypes.ts)

backend/src/scripts/generateTypes.ts
import fs from 'fs';
import path from 'path';
import { Model, ModelStatic, DataTypes } from 'sequelize';

const modelsDir = path.join(__dirname, '../models');
const outputPath = path.join(
  __dirname,
  '../../../frontend/src/types/models.ts'
);

function getTypeScriptType(sequelizeType: any): string {
  const type = sequelizeType.toString().toLowerCase();

  if (sequelizeType instanceof DataTypes.ENUM) {
    const enumType = sequelizeType as unknown as { values: string[] };
    return enumType.values.map((v) => `'${v}'`).join(' | ');
  }

  if (
    sequelizeType instanceof DataTypes.JSON ||
    sequelizeType instanceof DataTypes.JSONB
  ) {
    const jsonType = sequelizeType as unknown as { options?: { type: string } };
    if (jsonType.options?.type) {
      return jsonType.options.type;
    }
    return 'Record<string, unknown>';
  }

  if (type.includes('[]')) {
    const arrayType = sequelizeType as unknown as { type: any };
    if (arrayType.type) {
      const elementType = getTypeScriptType(arrayType.type);
      return `${elementType}[]`;
    }
    const match = type.match(/array<(.*?)>/i);
    if (match) {
      const elementType = getTypeScriptType({ toString: () => match[1] });
      return `${elementType}[]`;
    }
    return 'any[]';
  }

  if (type.includes('json')) {
    if (sequelizeType.options?.type) {
      return sequelizeType.options.type;
    }
    return 'Record<string, unknown>';
  }

  if (
    sequelizeType instanceof DataTypes.INTEGER ||
    sequelizeType instanceof DataTypes.BIGINT ||
    sequelizeType instanceof DataTypes.FLOAT ||
    sequelizeType instanceof DataTypes.DOUBLE
  ) {
    return 'number';
  }

  if (
    sequelizeType instanceof DataTypes.STRING ||
    sequelizeType instanceof DataTypes.TEXT ||
    sequelizeType instanceof DataTypes.UUID
  ) {
    return 'string';
  }

  if (sequelizeType instanceof DataTypes.BOOLEAN) {
    return 'boolean';
  }

  if (sequelizeType instanceof DataTypes.DATE) {
    // NOTE: 日付型はstringで表現する
    return 'string';
  }

  console.warn(`Unknown type: ${type}, using 'any'`);
  return 'any';
}

2. インターフェース生成ロジック

backend/src/scripts/generateTypes.ts
function generateTypeDefinition(model: ModelStatic<Model>) {
  const attributes = model.getAttributes();
  const defaultScope =
    (model as any).options?.defaultScope?.attributes?.exclude || [];
  let typeDefinition = `export interface ${model.name} {\n`;

  for (const [key, attribute] of Object.entries(attributes)) {
    if (defaultScope.includes(key)) continue;

    const tsType = getTypeScriptType(attribute.type);
    typeDefinition += `  ${key}${attribute.allowNull ? '?' : ''}: ${tsType};\n`;
  }

  typeDefinition += '}\n\n';
  return typeDefinition;
}

3. ファイル生成処理

backend/src/scripts/generateTypes.ts
(async function () {
  let output = '// This file is auto-generated. DO NOT EDIT.\n\n';

  fs.readdirSync(modelsDir)
    .filter((file) => file.endsWith('.ts') && file !== 'index.ts')
    .forEach((file) => {
      // eslint-disable-next-line @typescript-eslint/no-require-imports
      const modelModule = require(path.join(modelsDir, file));
      const model = modelModule.default;
      if (model) {
        output += generateTypeDefinition(model);
      }
    });
  fs.writeFileSync(outputPath, output);
  console.log('✨ Type definitions generated successfully!');
})();

生成される型定義の例

frontend/src/types/models.ts
// This file is auto-generated. DO NOT EDIT.

export interface User {
  id: string;
  username: string;
  createdAt: string;
  updatedAt: string;
}

export interface Part {
  id: number;
  type: string;
  prototypeVersionId: string;
  parentId?: number;
  name: string;
  description: string;
  color: string;
  position: Record<string, unknown>;
  width: number;
  height: number;
  // ... その他のプロパティ
}

自動更新の設定

Git pre-commitフックの設定

.husky/pre-commit
#!/usr/bin/env bash

cd backend

# 型定義を生成
npm run generate-types

cd ..

# 生成された型定義ファイルをステージングに追加
if [ -f "frontend/src/types/models.ts" ]; then
  git add frontend/src/types/models.ts
  echo "✨ Added frontend/src/types/models.ts to staging"
fi

exit 0

型定義生成の特徴

  1. 型の自動変換

    • Sequelizeの型からTypeScriptの型への適切な変換
    • ENUM、JSON、配列など複雑な型にも対応
    • Nullableな属性の考慮
  2. 柔軟な型定義

    • モデルの属性を動的に解析
    • カスタム型のサポート
    • デフォルトスコープの考慮
  3. 保守性の向上

    • コメントによる自動生成ファイルの明示
    • 型定義の一元管理
    • コミット時の自動更新

まとめ

Sequelizeモデルの型定義を自動生成・共有することで:

  • フロントエンド・バックエンド間の型安全性が向上
  • 開発効率が改善
  • 保守性が向上

が実現できます。

参考資料

2
0
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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?