类型债不是 TypeScript 的问题

很多团队上 TypeScript 后,前几个月体验很好:编辑器提示更清楚,重构更安心,接口字段也不容易写错。项目继续长大后,新的问题会出现:到处都是 any,接口返回类型和页面状态混在一起,后端字段一变前端大片报错,组件 props 越传越厚。

这类问题不是 TypeScript 没用,而是类型边界没有设计。TypeScript 只能帮你检查已经表达出来的约束,不能自动替团队决定哪些类型应该稳定、哪些类型只属于某个页面。

先分清三种类型

第一种是接口 DTO,也就是后端接口返回和提交的数据结构。它应该尽量贴近网络边界,字段名、可空性、枚举值都要和接口保持一致。

第二种是领域模型。它是前端真正用于业务判断的数据形态,可以把后端字段整理成更适合页面使用的结构。例如把字符串状态转成明确枚举,把时间字符串转成展示对象,把缺省字段补成默认值。

第三种是视图状态。它只服务于某个页面或组件,例如是否展开、是否选中、临时输入、校验错误。视图状态不要污染接口 DTO,否则类型会越来越难维护。

API 边界要做转换

大型项目里,最容易失控的是直接把接口返回类型一路传到组件深处。短期看少写代码,长期看每个组件都被后端字段绑住。接口字段改名、可空规则变化、枚举新增,影响会扩散到整个页面树。

更稳的做法是在 API 层或页面入口做一次转换。外部数据进来先变成前端模型,组件只依赖模型。这样后端字段变化时,优先修改转换层,而不是全项目搜索替换。

any 要有退出计划

完全禁止 any 在老项目里通常不现实,但每一个 any 都应该知道原因和范围。临时兼容第三方库、迁移旧模块、处理未知 JSON,都可以短期使用 unknownany,但要尽量把它包在边界层,不要传入核心业务组件。

如果一个 any 穿过三层函数还没有被收窄,它就已经变成类型债。解决方式不是一次清零,而是每次改相关代码时顺手收紧一小段。

表单类型要单独处理

表单输入经常是字符串、空值和半成品状态,不应该直接等同于提交 DTO。用户还没填完时,页面状态可能不满足后端要求。建议把表单草稿类型、校验后类型和提交 DTO 分开。

这样做的好处是错误更清楚:输入阶段允许空值,校验阶段收窄类型,提交阶段只接受合法 DTO。复杂表单尤其需要这个分层。

渐进收紧比一次重构更可靠

类型治理适合渐进推进。可以先从接口最多、变更最频繁的模块开始,建立 DTO、转换函数和页面模型。新代码按新规范写,老代码在修改时逐步迁移。

团队还可以加几条轻量规则:新增接口必须有返回类型;新增公共组件不能使用裸 any;工具函数要写清泛型边界;核心目录逐步开启更严格的 TypeScript 配置。

总结

TypeScript 的收益来自边界清楚,而不是类型文件数量多。DTO 守住接口边界,领域模型服务业务判断,视图状态留在页面内部,再配合持续收敛 any,大型前端项目的类型债才会慢慢下降。