一条评论的旅程:nelsonlai.dev 的 TypeScript 高级特性类型安全实践指南
2026/8/24 4:19:24 网站建设 项目流程

一条评论的旅程: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 里,评论接口的slugz.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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询