很多项目虽然使用TypeScript,但类型设计仍然停留在“给对象加一个interface”。等到草稿、发布、定时发布、作者权限和SEO字段都加入后,同一个Post类型会被列表、详情、表单和API反复使用,最后到处都是可选属性和类型断言。
我以前也会把接口数据直接放进表单,保存时原样发回去。这样开始很快,但CMS的读取模型和写入模型其实不是一回事。
不要让一个Post负责所有场景
type PostStatus = 'draft' | 'review' | 'scheduled' | 'published'
type Post = {
id: number
title: string
slug: string
content: string
excerpt: string
status: PostStatus
publishedAt: string | null
createdAt: string
updatedAt: string
}完整文章中一定存在的字段,不要为了省事全部加?。如果允许为空,用null明确表达。
单独设计表单与请求类型
type PostFormValues = {
title: string
slug: string
content: string
excerpt: string
status: PostStatus
categoryIds: number[]
tagIds: number[]
publishedAt: string | null
}
type CreatePostInput = PostFormValues
type UpdatePostInput = Partial<
Pick<PostFormValues,
'title' | 'slug' | 'content' | 'excerpt' |
'status' | 'categoryIds' | 'tagIds' | 'publishedAt'
>
>只有后台确实支持部分更新时,才使用Partial。类型应该符合真实API,而不是为了让前端写起来方便。
用联合类型表达状态规则
定时发布必须有时间,草稿则不需要。可以把规则写进类型:
type PublicationInput =
| { status: 'draft' | 'review'; publishedAt: null }
| { status: 'scheduled'; publishedAt: string }
| { status: 'published'; publishedAt: string }这样能减少“状态为scheduled但时间为空”这种无效组合。
as Post不会验证API数据
const post = await response.json() as Post这只是告诉编译器相信开发者,不会在运行时检查JSON。服务器字段改变、返回HTML错误页时,TypeScript无法阻止。
async function fetchPost(id: number): Promise<Post> {
const response = await fetch(`/api/posts/${id}`)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
return response.json()
}重要的系统边界可以使用Zod等运行时校验库。TypeScript负责静态检查,运行时校验负责不可信的外部数据,两者不能替代。
列表和详情分开
列表接口通常不返回完整正文,不应该仍然声明为Post[]。
type PostListItem = Pick<
Post,
'id' | 'title' | 'slug' | 'status' | 'publishedAt' | 'updatedAt'
>当列表字段变化时,就不会影响文章详情和编辑表单。
外部CMS数据先转换
WordPress REST API中的标题和正文可能包含rendered字段:
type WordPressPostResponse = {
id: number
slug: string
title: { rendered: string }
content: { rendered: string }
date: string
modified: string
}不要让所有组件都读取post.title.rendered,在API层统一转换。
function mapPost(source: WordPressPostResponse): PostViewModel {
return {
id: source.id,
slug: source.slug,
titleHtml: source.title.rendered,
contentHtml: source.content.rendered,
publishedAt: source.date,
updatedAt: source.modified
}
}外部结构变化时,影响会集中在转换层。rendered是HTML,输出时还要确认内容来源和清理策略。
日期先当字符串处理
JSON里的日期是字符串,不会自动成为Date。
type PostResponse = {
publishedAt: string | null
}业务需要时,在边界处转换。还要约定UTC或日本时间,不能只靠字段名判断。
用unknown代替不必要的any
type PostMeta = Record<string, unknown>
const value = post.meta['readingTime']
if (typeof value === 'number') {
console.log(value)
}unknown会要求使用者先判断,比any更适合来源不确定的自定义字段。
检查清单
- 读取模型和写入模型是否混用
null和可选属性是否有统一规则- 状态能否使用联合类型
- 列表与详情是否需要同一组字段
- API边界是否进行转换和校验
- 日期格式与时区是否明确
- 是否存在不必要的
any和as - 外部CMS结构是否泄漏到所有组件
- 更新接口是全量还是部分更新
总结
TypeScript并不是类型越复杂越好。真正有价值的类型设计,是让不应该出现的状态更难进入系统。
对于博客CMS,最好分清外部API响应、系统内部模型、编辑表单、创建与更新输入。代码会稍微增加,但接口变更、表单修改和问题调查都会更容易。

