一谈到 TypeScript 和 OpenAPI,团队里常见的争论很快会走向两个极端。第一种声音是“既然有 schema,就全量生成客户端和类型,不要再手写”;第二种声音是“生成代码太重、太脆,还是自己定义类型更灵活”。这两种说法都抓到了一部分事实,但都不够完整。
真实项目里,问题从来不是要不要生成,而是生成结果应该停在系统的哪一层。把生成类型一路带进页面和业务层,后面通常会被接口字段变化反复打断;完全手写类型,又会让服务端和前端逐渐讲不同的语言。TypeScript 与 OpenAPI 的协同,关键不是立场,而是边界设计。
先回答一个根问题:生成代码到底帮你省掉了什么
生成代码最擅长解决的是“重复同步”和“协议抄写”问题,比如:
- 请求参数和响应体字段不需要两边手抄。
- 接口新增字段后,可以更早暴露影响面。
- 基础请求方法、错误壳层和 SDK 结构可以保持统一。
但它并不自动解决业务语义问题。后端的 UserProfileDto、OrderItemResponse、CampaignSnapshot 这些结构,只能说明接口怎么长,不等于前端页面和领域逻辑就该直接围着这些 DTO 运转。
生成层和业务层之间,最好保留一层显式适配
比较稳的结构通常是三层:
- OpenAPI 生成层:负责原始请求方法和 DTO 类型。
- 适配层:把 DTO 转换成前端真正需要的领域对象或视图模型。
- 业务层 / 页面层:只依赖适配后的类型,不直接绑定生成结果。
type UserDto = components['schemas']['UserResponse']
type UserCard = {
id: string
displayName: string
isActive: boolean
}
function toUserCard(dto: UserDto): UserCard {
return {
id: dto.id,
displayName: dto.nickname ?? dto.name,
isActive: dto.status === 'active'
}
}
这层转换的价值,不是多写几个映射函数,而是把“接口怎么返回”和“页面怎么表达”分开。只要后端字段改动仍然能在适配层被消化,页面层就不必跟着大面积震荡。
什么适合生成,什么更适合手写
| 内容 | 更适合生成 | 更适合手写 |
|---|---|---|
| 原始 DTO 类型 | 是 | 否 |
| 请求方法签名 | 是 | 否 |
| 领域模型 / 视图模型 | 否 | 是 |
| 组合查询结果 | 否 | 是 |
| 统一错误语义 | 部分 | 通常需要手写归一化 |
这里最容易犯的错,是把生成结果误当成最终业务模型。生成的职责是保真,不是表达业务抽象。你真正要给页面和业务层提供的,通常应该是经过适配后的类型。
一个常见失败案例:页面层直接消费生成 DTO,改一个字段牵一串
某团队为追求“端到端一致”,让页面组件直接依赖 OpenAPI 生成的响应类型。开始几周看起来效率很高,后来后端做了很正常的一轮接口整理:字段分组、枚举值调整、可空策略修正。理论上只是协议优化,实际却导致大量页面组件、hooks 和状态存储一起修改。
根因不是后端改太多,而是前端没有设置缓冲层。只要 DTO 直接进入业务层,每一次接口演进都会变成 UI 演进,仓库自然越来越脆。
版本演进要重点盯三件事:新增、弃用和破坏式收窄
契约演进里最危险的不是接口新增字段,而是“看起来不大、实际上会卡住编译”的收窄变化,比如:
string | null收窄成string- 枚举值替换或删除
- 某些字段从可选改成必填,或反过来
- 嵌套对象结构被重组
比较稳的做法通常是:
- CI 对 OpenAPI 变更做 diff。
- 把 diff 区分为新增、兼容变更和破坏式变更。
- 前端优先在适配层集中吸收兼容改动。
- 真正的破坏式收窄必须配套迁移窗口和版本说明。
如果没有这套机制,团队就会在“明明只是改了接口文档,为什么前端炸了”里反复循环。
生成代码也要防止“生成物支配整个工程”
另一个容易被忽略的问题是:生成代码不应反过来决定你的工程结构。常见风险信号包括:
- 生成文件被手改,下一次生成又覆盖掉。
- 页面层自己 import 生成目录里的原始 DTO。
- 生成文件命名和业务命名完全不一致,导致认知负担很重。
- 每次后端微调,前端就要更新一堆毫无业务价值的类型引用。
只要这些现象开始出现,就说明生成物已经越过“工具层”进入了“业务层”。这时该优化的不是生成命令,而是边界。
可执行的协作清单
- 是否明确区分了生成层 DTO 和业务层模型。
- 页面和状态管理是否避免直接依赖生成目录。
- OpenAPI 变更是否进入 CI diff,而不是上线前才发现。
- 破坏式变更是否有迁移窗口和适配层缓冲。
- 生成代码是否保持可重建,而不是依赖手工补丁。
总结
TypeScript 和 OpenAPI 的协同,关键不是“全生成”还是“全手写”,而是知道生成层的边界在哪里。生成结果负责承接协议事实,手写适配负责表达业务语义,版本治理负责控制变更节奏。只要这三层拆开,契约协同就会从反复拉扯,变成可持续的团队工作流。
本系列导航:
- 如果你要先稳住对外导出边界,建议先看 TypeScript 公共 API 设计
- 若你还没把输入边界做实,接着读 TypeScript 运行时校验与静态类型协作
- 如果你想把契约继续延伸到事件层,再看 TypeScript 事件系统建模
延伸阅读:


