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

## はじめに

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）

````typescript: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. インターフェース生成ロジック

````typescript: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. ファイル生成処理

````typescript: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!');
})();
````


## 生成される型定義の例

````typescript: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フックの設定

````bash:.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モデルの型定義を自動生成・共有することで：
- フロントエンド・バックエンド間の型安全性が向上
- 開発効率が改善
- 保守性が向上

が実現できます。

## 参考資料
- [Sequelize Documentation](https://sequelize.org/)
- [TypeScript Documentation](https://www.typescriptlang.org/)
- [Husky - Git hooks](https://typicode.github.io/husky/)