はじめに
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
型定義生成の特徴
-
型の自動変換
- Sequelizeの型からTypeScriptの型への適切な変換
- ENUM、JSON、配列など複雑な型にも対応
- Nullableな属性の考慮
-
柔軟な型定義
- モデルの属性を動的に解析
- カスタム型のサポート
- デフォルトスコープの考慮
-
保守性の向上
- コメントによる自動生成ファイルの明示
- 型定義の一元管理
- コミット時の自動更新
まとめ
Sequelizeモデルの型定義を自動生成・共有することで:
- フロントエンド・バックエンド間の型安全性が向上
- 開発効率が改善
- 保守性が向上
が実現できます。