codegen-over-complex-types

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Consider Codegen as an Alternative to Complex Types

考虑用代码生成替代复杂类型

Overview

概述

Sometimes the best type-level code is no type-level code at all. When types become extremely complex, when they mirror external schemas (APIs, databases, protocols), or when they need to stay synchronized with changing external sources, code generation is often a better solution than sophisticated type-level programming.
Code generation trades compile-time complexity for build-time generation, often resulting in simpler, more maintainable code that stays in sync with its source of truth.
有时候最好的类型层级代码就是完全不写类型层级代码。当类型变得极为复杂、类型与外部结构(如API、数据库、协议)一致,或者类型需要与不断变化的外部源保持同步时,代码生成通常比复杂的类型层级编程更合适。
代码生成将编译时的复杂度转换为构建时的生成操作,通常能得到更简洁、更易维护且与真实数据源保持同步的代码。

When to Use This Skill

何时使用该技巧

  • Types mirror external schemas (OpenAPI, GraphQL, database)
  • Type-level logic becomes extremely complex
  • Types need to stay synchronized with external sources
  • Type maintenance cost exceeds value
  • Team struggles with complex type-level code
  • 类型与外部结构(OpenAPI、GraphQL、数据库)一致
  • 类型层级逻辑变得极为复杂
  • 类型需要与外部源保持同步
  • 类型维护成本超过其价值
  • 团队难以处理复杂的类型层级代码

The Iron Rule

铁则

When types become too complex or must stay synchronized with external sources, generate them from schemas rather than writing sophisticated type-level code.
当类型过于复杂或必须与外部源保持同步时,从结构生成类型,而非编写复杂的类型层级代码。

Detection

识别信号

Watch for these signals:
typescript
// 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 */;
typescript
// 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 */;

The Complexity Trade-off

复杂度权衡

typescript
// 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
typescript
// 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

Generating from OpenAPI

从OpenAPI生成类型

bash
undefined
bash
undefined

Generate TypeScript from OpenAPI schema

Generate TypeScript from OpenAPI schema

npm install -D openapi-typescript npx openapi-typescript schema.yaml -o src/api-types.ts

```typescript
// 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;
    };
  };
}
npm install -D openapi-typescript npx openapi-typescript schema.yaml -o src/api-types.ts

```typescript
// 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;
    };
  };
}

Generating from GraphQL

从GraphQL生成类型

bash
undefined
bash
undefined

Generate TypeScript from GraphQL schema

Generate TypeScript from GraphQL schema

npm install -D @graphql-codegen/cli @graphql-codegen/typescript

```yaml
npm install -D @graphql-codegen/cli @graphql-codegen/typescript

```yaml

codegen.yml

codegen.yml

schema: schema.graphql generates: src/generated/graphql.ts: plugins: - typescript - typescript-operations

```typescript
// 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;
};
schema: schema.graphql generates: src/generated/graphql.ts: plugins: - typescript - typescript-operations

```typescript
// 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;
};

Generating from Database Schemas

从数据库结构生成类型

bash
undefined
bash
undefined

Generate TypeScript from database

Generate TypeScript from database

npm install -D prisma npx prisma generate

```typescript
// 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
}
npm install -D prisma npx prisma generate

```typescript
// 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
}

When to Choose Codegen

何时选择代码生成

Choose code generation when:
typescript
// 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
选择代码生成的场景:
typescript
// 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

When to Choose Type-Level Code

何时选择类型层级代码

Choose type-level programming when:
typescript
// 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
选择类型层级编程的场景:
typescript
// 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

Keeping Generated Types in Sync

保持生成类型的同步

json
// 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"
  }
}
yaml
undefined
json
// 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"
  }
}
yaml
undefined

.github/workflows/ci.yml

.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
undefined
  • name: Check generated types are up to date run: | npm run generate:api git diff --exit-code src/api-types.ts
undefined

Hybrid Approach

混合方案

typescript
// 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>;
typescript
// 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>;

Pressure Resistance Protocol

决策流程

When deciding between type-level code and codegen:
  1. Assess complexity: Will the type-level code be maintainable?
  2. Check source of truth: Is there an external schema?
  3. Consider team: Can the team maintain complex type code?
  4. Evaluate drift risk: How often will types need updating?
  5. Prototype both: Try both approaches, compare maintainability
在类型层级代码和代码生成之间做选择时:
  1. 评估复杂度:类型层级代码是否易于维护?
  2. 检查真实数据源:是否存在外部结构?
  3. 考虑团队情况:团队能否维护复杂的类型代码?
  4. 评估不一致风险:类型需要多久更新一次?
  5. 两种方案都尝试:分别试用两种方案,对比可维护性

Red Flags

危险信号

SymptomProblemSolution
100+ line type definitionsToo complexGenerate from schema
Manual updates for API changesDrift riskAuto-generate from API
Team avoids touching type filesToo complexSimplify or generate
Types out of sync with backendNo single sourceGenerate from schema
症状问题解决方案
超过100行的类型定义过于复杂从结构生成类型
API变更时需要手动更新类型存在不一致风险从API自动生成类型
团队避免修改类型文件过于复杂简化或生成类型
类型与后端不一致缺乏单一真实数据源从结构生成类型

Common Rationalizations

常见顾虑

"I don't want a build step"

"我不想增加构建步骤"

Reality: Modern development already has build steps. Type generation is fast and integrates into existing workflows.
实际情况:现代开发流程本来就包含构建步骤。类型生成速度快,且能融入现有工作流。

"Generated code is ugly"

"生成的代码很丑陋"

Reality: You don't read generated code, you use it. The types it produces are clean and well-typed.
实际情况:你不需要阅读生成的代码,只需要使用它。它生成的类型清晰且类型完善。

"I can write better types by hand"

"我手动写的类型更好"

Reality: You might write nicer types initially, but generated types stay in sync automatically.
实际情况:你最初可能写出更优雅的类型,但生成的类型能自动保持同步。

Quick Reference

快速参考

SourceToolCommand
OpenAPIopenapi-typescript
npx openapi-typescript schema.yaml -o types.ts
GraphQL@graphql-codegen
npx graphql-codegen
DatabasePrisma
npx prisma generate
JSON Schemajson-schema-to-typescript
npx json2ts schema.json -o types.ts
Protobufprotobuf-ts
npx protoc --ts_out
来源工具命令
OpenAPIopenapi-typescript
npx openapi-typescript schema.yaml -o types.ts
GraphQL@graphql-codegen
npx graphql-codegen
数据库Prisma
npx prisma generate
JSON Schemajson-schema-to-typescript
npx json2ts schema.json -o types.ts
Protobufprotobuf-ts
npx protoc --ts_out

The Bottom Line

总结

When types become too complex or must stay synchronized with external sources, generate them. Code generation trades build complexity for maintainability and correctness.
当类型过于复杂或必须与外部源保持同步时,选择生成类型。代码生成以构建时的复杂度换取可维护性和准确性。

Reference

参考资料

  • Effective TypeScript, 2nd Edition by Dan Vanderkam
  • Item 58: Consider Codegen as an Alternative to Complex Types
  • Effective TypeScript, 2nd Edition by Dan Vanderkam
  • Item 58: Consider Codegen as an Alternative to Complex Types