关于 adopting-generated-api-types
面向 PostHog 前端开发者的技能,指导将手写 API 调用和类型迁移到 OpenAPI 生成的 TypeScript 函数与类型,并提供兼容性检查和验证步骤。
主要功能
- 01识别三类手写 API 调用
- 02查找生成的函数和类型
- 03检查类型兼容性并迁移调用
- 04清理旧类型并运行验证
适用场景
- 01替换旧式 API 请求
- 02将手写响应类型换为生成类型
- 03后端 Schema 改进后的前端整理
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ posthog/ posthog --skill adopting-generated-api-types安装后在 Claude Code 中输入/adopting-generated-api-types确认已加载。
附带文件references/migration-patterns.mdreferences/type-compatibility.md
详细介绍
采用生成的 API 类型
概述
PostHog 通过 OpenAPI 流水线从 Django serializers 生成 TypeScript API 客户端函数和类型:
Django serializer → drf-spectacular → OpenAPI JSON → Orval → TypeScript (api.ts + api.schemas.ts + api.zod.ts)
生成的文件位于:
- 核心:
frontend/src/generated/core/api.ts、api.schemas.ts和api.zod.ts - 产品:
products/<product>/frontend/generated/api.ts、api.schemas.ts和api.zod.ts
生成的类型使用 Api 后缀(DashboardApi、SurveyApi)。手写类型从不使用该后缀。
本技能指导如何将手动 API 调用和手写类型替换为生成的等价物。
需要迁移的三种手动模式
旧版 frontend/src/lib/api.ts(约 6000 行)包含三层,全部都是迁移目标:
1. 高层对象 API(最常见)
api 对象上特定领域的便捷方法:
api.surveys.get(id)
api.surveys.create(data)
api.dashboards.list()
api.cohorts.update(id, data)
api.actions.create(data)
这是使用最广泛的模式 — 每个实体都有自己的命名空间,包含 CRUD 以及自定义方法(例如 api.surveys.getResponsesCount()、api.dashboards.streamTiles())。
2. 带手动 URL 的原始 HTTP 方法
api.get<SomeType>(`api/projects/${id}/surveys/`)
api.create<SomeType>(`api/projects/${id}/surveys/`, data)
api.update<SomeType>(url, data)
api.put<SomeType>(url, data)
api.delete(url)
3. ApiRequest 构建器(流式 URL 构造)
const url = new ApiRequest().surveys().assembleFullUrl()
const response = await api.get(url)
// or directly:
await new ApiRequest().survey(surveyId).withAction('summarize_responses').create({ data })
在可用的情况下,这三种模式都应替换为生成的函数。
何时使用
- 修改调用
api.<entity>.<method>()的文件(例如api.surveys.get()) - 修改调用
api.get<T>(...)、api.create<T>(...)等的文件 - 修改使用
new ApiRequest()构造 URL 的文件 - 修改从
~/types导入手写接口以表示 API 响应结构的文件 - 在后端 serializer 改进后清理前端代码
分步工作流
1. 确定手动调用做了什么
查看现有调用并提取:
- HTTP 方法 — GET、POST、PUT、PATCH、DELETE
- 实体和操作 — 什么资源,什么操作
- 类型参数 — 用于响应的手写类型
2. 找到生成的等价物
生成的函数名遵循 {resource}{Action} 约定:
surveysList — GET /api/projects/{id}/surveys/
surveysCreate — POST /api/projects/{id}/surveys/
surveysRetrieve — GET /api/projects/{id}/surveys/{id}/
surveysPartialUpdate — PATCH /api/projects/{id}/surveys/{id}/
surveysDestroy — DELETE /api/projects/{id}/surveys/{id}/
搜索位置:
- 核心端点:
frontend/src/generated/core/api.ts - 产品端点:
products/<product>/frontend/generated/api.ts
搜索策略:
- 在生成的
api.ts文件中 grep 实体名称 - 通过
get*Url辅助函数搜索 — 每个生成的函数上方都有对应的 URL 构建器 - 在
api.schemas.ts中搜索带Api后缀的类型名称
如果不存在生成的函数,后端端点可能缺少 @extend_schema 或 @validated_request。先使用 improving-drf-endpoints 技能修复后端,然后运行 hogli build:openapi。
自定义操作(如 api.surveys.summarize_responses())如果后端 @action 缺少 @extend_schema,可能没有生成的等价物。先检查生成的文件;如果缺失,则修复后端。
3. 检查类型兼容性
将手写类型与生成的 Api 类型进行比较。主要差异:
readonly修饰符 — 生成的类型会标记只读字段- 可选与必需 — 生成的类型精确反映
required= - 可空性 —
null类型是显式的 - 额外字段 — 生成的类型可能包含手写类型省略的字段
详情参见 type-compatibility.md。
4. 替换调用
详细的替换前后示例参见 migration-patterns.md,涵盖:
- 高层对象 API(
api.surveys.get()→surveysRetrieve()) - 原始 HTTP 方法(
api.get<T>(url)→ 生成的函数) - ApiRequest 构建器 → 生成的函数
- 分页列表调用
- 带请求体的创建/更新
- 删除调用
- Kea logic 加载器和监听器
- 带中止信号的调用
5. 在使用位置替换类型
将下游引用从手写类型更新为生成的类型:
// Before
function renderSurvey(survey: Survey): JSX.Element { ... }
// After
function renderSurvey(survey: SurveyApi): JSX.Element { ... }
6. 清理无用类型
迁移完某个手写类型的所有用法后:
- 从
types或本地文件中移除该类型定义 - 移除未使用的导入
- 运行
pnpm --filter=@posthog/frontend types:check验证没有破坏任何内容
决策指南
| 场景 | 操作 |
|---|---|
| 生成的函数存在 | 用生成的函数替换手动调用 |
| 生成的类型存在但函数不存在 | 将生成的类型用作手动调用的泛型参数,并提交后续任务以添加 @extend_schema |
| 两者都不存在 | 保留手动模式,先修复后端 serializer/viewset |
| 自定义 action 没有对应的生成等价物 | 保留 api.<>.<method>() 调用,先修复后端的 @action 注解 |
| 生成的类型与手写类型结构不同 | 调整调用点以适应生成的类型——serializer 是唯一事实来源 |
| 代码会修改响应对象 | 使用本地可变副本:const mutable = { ...response } 并修改它 |
| 同时需要读取和写入类型 | 读取使用 FooApi,通过 Parameters<typeof fooCreate>[1] 派生写入类型,或使用 PatchedFooApi |
导入约定
// Core generated functions — import from api.ts
import { domainsList, domainsCreate, domainsRetrieve } from '~/generated/core/api'
// Core generated types — import type from api.schemas.ts
import type { OrganizationDomainApi } from '~/generated/core/api.schemas'
// Core generated Zod schemas — import from api.zod.ts
import { DomainsCreateBody } from '~/generated/core/api.zod'
// Product generated functions — NO tilde prefix, use 'products/' path
import { surveysList, surveysRetrieve } from 'products/surveys/frontend/generated/api'
import type { SurveyApi } from 'products/surveys/frontend/generated/api.schemas'
import { SurveysCreateBody } from 'products/surveys/frontend/generated/api.zod'
// Within a product, relative imports also work
import { logsAlertsCreate } from '../generated/api'
import type { LogsAlertConfigurationApi } from '../generated/api.schemas'
import { LogsAlertsCreateBody } from '../generated/api.zod'
路径规则:
- 核心:
~/generated/core/...(波浪号前缀) - 外部产品:
products/<product>/frontend/generated/...(无波浪号) - 内部产品:相对路径
../generated/...或./generated/...
使用 import type 导入类型以启用正确的 tree-shaking。
生成的函数在底层如何工作
生成的函数通过 api-orval-mutator.ts 包装同一个 api 模块:
surveysList(projectId, params)
→ apiMutator(url, { method: 'GET' })
→ api.get(url)
切换到生成的函数不会改变 HTTP 行为——相同的 cookie、相同的 CSRF、相同的错误处理。唯一的区别是类型安全和 URL 构造。
验证迁移
- TypeScript 检查:
pnpm --filter=@posthog/frontend types:check - 搜索遗留的手动类型: 在整个代码库中搜索旧类型名
- 运行相关测试:
hogli test <test>
相关
- 后端侧: 使用
improving-drf-endpoints修复产生劣质类型的 serializer - 类型系统文档:
docs/published/handbook/engineering/type-system - API mutator:
frontend/src/lib/api-orval-mutator.ts - 重新生成类型:
hogli build:openapi