一条评论的旅程:nelsonlai.dev 的 TypeScript 高级特性类型安全实践指南
【免费下载链接】nelsonlai.devMy personal website built with TypeScript, Next.js, and TailwindCSS项目地址: https://gitcode.com/gh_mirrors/ho/nelsonlai.dev
nelsonlai.dev 是一个基于 Next.js + TypeScript 构建的个人站点。这篇文章顺着"一条评论从数据库流向页面"的路线,讲清楚 TypeScript 高级特性是怎么让类型安全贯穿全程的。读完你能避开两个大坑:改了字段名没人提醒、前端盲猜后端返回结构。
两个翻车现场:改个字段名,为什么没人提醒?
第一个现场:后端把字段avatarUrl改成image,前端照样编译通过,上线后头像一片裂图。第二个现场:前端对着接口文档猜返回结构,写了一堆if (data.xxx !== undefined)的防御代码,后端改个字段名,页面直接白屏。
病根都一样:数据跨层流动,但"长什么样"只存在于口头约定里。TypeScript 高级特性要做的就一件事——把口头约定变成代码,让编译器在你上线前先开口。这也是 TS 最佳实践里最划算的一步:把该人肉盯的活儿交给编译器。
第一站·数据出生地 🏭:3 行代码锁定表字段类型
评论表定义在 src/db/schemas/comment.schema.ts。Drizzle 建表像画"数据形状的图纸":字段类型、是否可空、外键约束,全写死在代码里(节选):
export const comments = pgTable('comments', { body: text('body').notNull(), userId: text('user_id').notNull().default('ghost').references(() => users.id), postId: text('post_id').notNull().references(() => posts.slug), parentId: text('parent_id'), })从这一刻起,代码里任何碰这张表的地方,编译器都知道:body必须是字符串、userId匿名时兜底为'ghost'、postId外键删文章会级联删评论。评论还没出生,类型安全已经锁了第一道。
第二站·数据过安检 🛂:输入严格,输出克制
数据要出数据库、走 API,这站的规矩就八个字:输入严格,输出克制。
输入侧,Zod 校验像机场安检:不在清单上的东西,门都进不了。src/orpc/schemas/comment.schema.ts 里,评论接口的slug是z.string().min(1),sort只接受'newest' | 'oldest'且给了默认值。前端传错参数,业务逻辑执行前就被拦下,不用在代码里写防御式if。
输出侧用 Pick。Pick 像点菜,只挑客人要的几样。接口要带用户信息,但只点四个字段:
user: createSelectSchema(users).pick({ id: true, name: true, image: true, role: true, })email这类不该外流的字段天然不存在。更妙的是:oRPC 直接从 Zod schema 推断出 TS 类型,前端拿到的"后端会返回什么"不再是猜的,是编译器盖章的合同。
第三站·数据上舞台 🎭:泛型与联合类型,让一个组件吃下所有数据
组件层是泛型、联合类型、类型守卫的主场。
泛型像个"占位符":组件写一次,每处业务塞自己的形状。管理后台的表格列定义,评论表是ColumnDef<Comment>,用户表是ColumnDef<User>(见 src/components/tables/users/columns.tsx),同一个表格组件通吃。而且Comment类型不是手写的,是从 oRPC 输出类型里推断出来的——源头一变,全链路跟着变。
联合类型描述"可能是 A,也可能是 B"。博客里评论和回复共用同一张表,靠parentId是否为空区分;接口参数也写成type: 'comments' | 'replies',传别的一律拒绝。
src/components/ui/link.tsx 里的 Link 组件是教科书案例,联合类型、条件类型、类型守卫一次看全(节选):
type InternalLink = '/' | `/${string}` type ExternalLink = `http://${string}` | `https://${string}` type ValidLink = InternalLink | ExternalLink export function Link<THref extends ValidLink>(props: LinkProps<THref>) { if (isExternalLink(props)) { return <a href={props.href} target='_blank' rel='noopener noreferrer' /> } return <LocalizedLink {...props} /> }isExternalLink就是类型守卫:运行时看 href 前缀做判断,编译期把 props 收窄成对应的形状,后续代码自动获得正确的自动补全。传一个非法链接?组件直接抛错,错误在舞台上现形,而不是流到用户手里。
收获小结:哪个特性解决什么问题
| 特性 | 解决的问题 | 什么时候该用 |
|---|---|---|
| Drizzle 类型化建表 | 字段类型与约束在出生地锁死,杜绝拼错字段名 | 定义数据库表、外键、索引时 |
| Zod 校验 | 输入在进业务逻辑前被运行时验证 | 每个 API 的入参、出参 |
| Pick | 输出只带必需字段,隐私字段天然不外流 | 裁剪用户信息等嵌套输出时 |
| 泛型 | 一个组件/函数适配多种数据形状 | 表格、列表、Link 这类通用 UI |
| 联合类型 + 类型守卫 | 表达"可能是 A 也可能是 B",运行时收窄 | 区分评论/回复、站内/站外链接 |
上手指南:clone 下来,从哪读起
git clone https://gitcode.com/gh_mirrors/ho/nelsonlai.dev
建议从 src/db/schemas/comment.schema.ts 读起看数据出生地,再到 src/orpc/schemas/comment.schema.ts 看过安检,最后到 src/orpc/procedures.ts 看 schema 是怎么绑到接口上的。顺着一条评论走完这三个文件,文中这些高级特性就都亲手摸过了。
【免费下载链接】nelsonlai.devMy personal website built with TypeScript, Next.js, and TailwindCSS项目地址: https://gitcode.com/gh_mirrors/ho/nelsonlai.dev
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考