codegen-over-complex-types について
複雑な型レベルコードを書く代わりに、スキーマから型を生成する判断を支援する TypeScript 開発向けのスキルです。OpenAPI、GraphQL、データベーススキーマを例に、生成型を同期する方法も扱います。
主な機能
- 01複雑な型でコード生成を検討
- 02OpenAPI と GraphQL から型を生成
- 03データベーススキーマから型を生成
- 04ビルドワークフローに生成処理を組み込む
- 05生成型とアプリ固有の型を組み合わせる
活用シーン
- 01外部 API スキーマを TypeScript に反映
- 02複雑な型レベルロジックの削減
- 03スキーマ変更後の型の再生成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill codegen-over-complex-typesインストール後、Claude Code で/codegen-over-complex-typesを入力して読み込まれたか確認します。
詳細
複雑な型の代替としてコード生成を検討する
概要
型レベルのコードは、まったく書かないのが最善である場合もあります。型が非常に複雑になる場合、外部スキーマ(API、データベース、プロトコル)を反映している場合、または変化する外部ソースとの同期が必要な場合は、高度な型レベルプログラミングよりもコード生成が適していることがよくあります。
コード生成では、コンパイル時の複雑さをビルド時の生成に置き換えます。その結果、よりシンプルで保守しやすく、信頼できる唯一の情報源と同期したコードになることがよくあります。
このスキルを使う場面
- 型が外部スキーマ(OpenAPI、GraphQL、データベース)を反映している
- 型レベルのロジックが非常に複雑になっている
- 型を外部ソースと同期させる必要がある
- 型の保守コストがその価値を上回っている
- チームが複雑な型レベルのコードに苦労している
鉄則
型が複雑になりすぎている場合や、外部ソースとの同期が必要な場合は、高度な型レベルのコードを手書きするのではなく、スキーマから生成してください。
検出
次の兆候に注意してください。
// RED FLAGS - Complex types that might be generated
// 50+ lines of conditional types to parse a URL
type ParseURL<T> = /* extremely complex type-level parser */;
// Types manually maintained to match API
type APIResponse = {
// 100+ fields that must match backend
// Every API change requires manual updates
};
// Types derived from JSON Schema via complex mappings
type FromSchema<T> = /* recursive conditional mapped type */;
複雑さのトレードオフ
// OPTION 1: Complex type-level code (maintainability cost)
type ParseOpenAPI<Schema> = Schema extends {
paths: infer Paths
} ? {
[Path in keyof Paths]: Paths[Path] extends {
[Method in 'get' | 'post' | 'put' | 'delete']: {
responses: infer Responses
}
} ? {
[Method in keyof Paths[Path]]: Responses extends {
200: { content: { 'application/json': infer Body } }
} ? Body : never
} : never
} : never;
// 50+ more lines of type-level logic...
// OPTION 2: Generated types (build-time cost)
// Generated from OpenAPI schema:
interface GetUserResponse { /* ... */ }
interface CreateUserRequest { /* ... */ }
// Clear, debuggable, always in sync
OpenAPI からの生成
# Generate TypeScript from OpenAPI schema
npm install -D openapi-typescript
npx openapi-typescript schema.yaml -o src/api-types.ts
// Generated types - always in sync with API
export interface paths {
"/users": {
get: {
responses: {
200: {
content: {
"application/json": components["schemas"]["UserList"];
};
};
};
};
post: {
requestBody: {
content: {
"application/json": components["schemas"]["CreateUserRequest"];
};
};
};
};
}
export interface components {
schemas: {
User: {
id: string;
name: string;
email: string;
};
UserList: {
users: components["schemas"]["User"][];
total: number;
};
};
}
GraphQL からの生成
# Generate TypeScript from GraphQL schema
npm install -D @graphql-codegen/cli @graphql-codegen/typescript
# codegen.yml
schema: schema.graphql
generates:
src/generated/graphql.ts:
plugins:
- typescript
- typescript-operations
// Generated types from GraphQL schema
export type User = {
__typename?: 'User';
id: Scalars['ID']['output'];
name: Scalars['String']['output'];
email: Scalars['String']['output'];
posts?: Maybe<Array<Maybe<Post>>>;
};
export type GetUserQueryVariables = {
id: Scalars['ID']['input'];
};
export type GetUserQuery = {
__typename?: 'Query';
user?: {
__typename?: 'User';
id: string;
name: string;
} | null;
};
データベーススキーマからの生成
# Generate TypeScript from database
npm install -D prisma
npx prisma generate
// Generated from database schema
export type User = {
id: string
email: string
name: string | null
posts: Post[]
}
export type Post = {
id: string
title: string
content: string | null
published: boolean
author: User
authorId: string
}
コード生成を選ぶ場面
次の場合はコード生成を選んでください。
// 1. Source of truth is external
// API schema, database, protocol buffer definition
// → Generate types, don't write them
// 2. Types would be extremely complex
// Type-level URL parser, complex state machines
// → Generate simple types instead
// 3. Synchronization is critical
// API changes must be reflected in types
// → CI generates types from schema
// 4. Team type-level expertise is limited
// Complex type code is hard to maintain
// → Generated code is easier to understand
型レベルのコードを選ぶ場面
次の場合は型レベルプログラミングを選んでください。
// 1. Deriving from existing TypeScript types
// Deriving variants, transformations of your own types
// → Type-level code is appropriate
// 2. Simple transformations
// Pick, Omit, Partial - standard utilities
// → Type-level is simpler than codegen
// 3. No external source of truth
// Internal domain models
// → Write types directly
// 4. Need runtime flexibility
// Types depend on runtime values
// → Type-level code can handle this
生成された型を同期させる
// package.json
{
"scripts": {
"generate:api": "openapi-typescript api.yaml -o src/api-types.ts",
"generate:db": "prisma generate",
"build": "npm run generate:api && npm run generate:db && tsc",
"predev": "npm run generate:api && npm run generate:db"
}
}
# .github/workflows/ci.yml
- name: Check generated types are up to date
run: |
npm run generate:api
git diff --exit-code src/api-types.ts
ハイブリッドアプローチ
// Generate base types from external source
import type { User as GeneratedUser } from './generated/api';
// Extend with application-specific types
interface User extends GeneratedUser {
// Add computed properties
displayName: string;
}
// Derive using type-level code
type UserInput = Omit<User, 'id' | 'createdAt'>;
type UserUpdate = Partial<UserInput>;
プレッシャーへの対処プロトコル
型レベルのコードとコード生成のどちらにするかを決める際は、次の点を確認してください。
- 複雑さを評価する:型レベルのコードは保守可能ですか?
- 信頼できる唯一の情報源を確認する:外部スキーマはありますか?
- チームについて考慮する:チームは複雑な型コードを保守できますか?
- 乖離のリスクを評価する:型の更新はどのくらいの頻度で必要になりますか?
- 両方を試作する:両方のアプローチを試し、保守性を比較してください
注意すべき兆候
| 症状 | 問題 | 解決策 |
|---|---|---|
| 100 行を超える型定義 | 複雑すぎる | スキーマから生成する |
| API の変更を手動で反映している | 乖離のリスク | API から自動生成する |
| チームが型ファイルに触れるのを避けている | 複雑すぎる | シンプルにするか、生成する |
| 型がバックエンドと同期していない | 単一の情報源がない | スキーマから生成する |
よくある言い訳
「ビルドステップを増やしたくない」
実際には:現在の開発環境には、すでにビルドステップがあります。型生成は高速で、既存のワークフローに組み込めます。
「生成されたコードは見栄えが悪い」
実際には:生成されたコードを読むのではなく、使用するのです。生成される型は明快で、適切に型付けされています。
「手書きのほうがよい型を書ける」
実際には:最初はより整った型を書けるかもしれませんが、生成された型は自動的に同期され続けます。
クイックリファレンス
| ソース | ツール | コマンド |
|---|---|---|
| OpenAPI | openapi-typescript | npx openapi-typescript schema.yaml -o types.ts |
| GraphQL | @graphql-codegen | npx graphql-codegen |
| データベース | Prisma | npx prisma generate |
| JSON Schema | json-schema-to-typescript | npx json2ts schema.json -o types.ts |
| Protobuf | protobuf-ts | npx protoc --ts_out |
要点
型が複雑になりすぎている場合や、外部ソースとの同期が必要な場合は、型を生成してください。コード生成はビルドの複雑さを保守性と正確性に置き換えます。
参考資料
- Dan Vanderkam 著『Effective TypeScript, 2nd Edition』
- 項目 58:複雑な型の代替としてコード生成を検討する