关于 codegen-over-complex-types
一项 TypeScript 开发技能,说明何时应从模式生成类型,而非编写复杂的类型级代码。内容涵盖 OpenAPI、GraphQL 和数据库模式,以及保持生成类型同步的方法。
主要功能
- 01评估复杂类型是否适合代码生成
- 02从 OpenAPI 和 GraphQL 生成类型
- 03从数据库模式生成类型
- 04将生成步骤纳入构建工作流
- 05组合生成类型与应用专用类型
适用场景
- 01在 TypeScript 中映射外部 API 模式
- 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》第 2 版
- 条目 58:将代码生成视为复杂类型的替代方案
评论
支持 Markdown