mcpField
C
codegen-over-complex-types

codegen-over-complex-types

github.com/marius-townhouse開発645 回インストール39 週間前に更新
リポジトリ

codegen-over-complex-types について

複雑な型レベルコードを書く代わりに、スキーマから型を生成する判断を支援する TypeScript 開発向けのスキルです。OpenAPI、GraphQL、データベーススキーマを例に、生成型を同期する方法も扱います。

主な機能

  1. 01複雑な型でコード生成を検討
  2. 02OpenAPI と GraphQL から型を生成
  3. 03データベーススキーマから型を生成
  4. 04ビルドワークフローに生成処理を組み込む
  5. 05生成型とアプリ固有の型を組み合わせる

活用シーン

  1. 01外部 API スキーマを TypeScript に反映
  2. 02複雑な型レベルロジックの削減
  3. 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>;

プレッシャーへの対処プロトコル

型レベルのコードとコード生成のどちらにするかを決める際は、次の点を確認してください。

  1. 複雑さを評価する:型レベルのコードは保守可能ですか?
  2. 信頼できる唯一の情報源を確認する:外部スキーマはありますか?
  3. チームについて考慮する:チームは複雑な型コードを保守できますか?
  4. 乖離のリスクを評価する:型の更新はどのくらいの頻度で必要になりますか?
  5. 両方を試作する:両方のアプローチを試し、保守性を比較してください

注意すべき兆候

症状問題解決策
100 行を超える型定義複雑すぎるスキーマから生成する
API の変更を手動で反映している乖離のリスクAPI から自動生成する
チームが型ファイルに触れるのを避けている複雑すぎるシンプルにするか、生成する
型がバックエンドと同期していない単一の情報源がないスキーマから生成する

よくある言い訳

「ビルドステップを増やしたくない」

実際には:現在の開発環境には、すでにビルドステップがあります。型生成は高速で、既存のワークフローに組み込めます。

「生成されたコードは見栄えが悪い」

実際には:生成されたコードを読むのではなく、使用するのです。生成される型は明快で、適切に型付けされています。

「手書きのほうがよい型を書ける」

実際には:最初はより整った型を書けるかもしれませんが、生成された型は自動的に同期され続けます。

クイックリファレンス

ソースツールコマンド
OpenAPIopenapi-typescriptnpx openapi-typescript schema.yaml -o types.ts
GraphQL@graphql-codegennpx graphql-codegen
データベースPrismanpx prisma generate
JSON Schemajson-schema-to-typescriptnpx json2ts schema.json -o types.ts
Protobufprotobuf-tsnpx protoc --ts_out

要点

型が複雑になりすぎている場合や、外部ソースとの同期が必要な場合は、型を生成してください。コード生成はビルドの複雑さを保守性と正確性に置き換えます。

参考資料

  • Dan Vanderkam 著『Effective TypeScript, 2nd Edition』
  • 項目 58:複雑な型の代替としてコード生成を検討する

コメント