尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Zod 实战指南:3 步搞定 TypeScript 数据校验,从表单到 API 入参

Zod 实战指南:3 步搞定 TypeScript 数据校验,从表单到 API 入参 Zod 实战指南3 步搞定 TypeScript 数据校验从表单到 API 入参【免费下载链接】zodTypeScript-first schema validation with static type inference项目地址: https://gitcode.com/GitHub_Trending/zo/zod凌晨三点线上报警一个字段没做类型校验把字符串 100 当成了数字存进数据库下游服务直接炸掉。这类事故的共同点是数据在运行时绕过了一切静态检查。Zod 就是一个解决这个问题的库用 TypeScript 声明模式schema再对任意未知数据做运行时校验和类型推断输入不可信输出强类型。先搞清楚Zod 数据校验能替你做什么装一下就够用整个库零外部依赖npm install zod定义一个 schema 并解析数据Zod 数据校验的基本单元是 schema。这段代码做的事是声明一个玩家对象的形状然后用parse校验任意输入。import * as z from zod; // 声明结构username 必须是字符串xp 必须是数字 const Player z.object({ username: z.string(), xp: z.number(), }); // 输入是 unknown不信任任何来源 const input: unknown { username: billie, xp: 100 }; // 校验通过返回强类型的深拷贝 const data Player.parse(input); console.log(data.username); // billie搞定了输入进来怎么办接下来看类型从哪来。从 schema 推断 TypeScript 类型不用再手写 interface类型直接从模式推导出来。这段代码做的事是把 schema 推断出的类型提取出来当作普通类型使用。import * as z from zod; const Player z.object({ username: z.string(), xp: z.number(), }); // 提取推断类型{ username: string; xp: number } type Player z.infertypeof Player; const player: Player { username: billie, xp: 100 };有转换逻辑时输入和输出类型会分开z.input/z.output可以分别提取。类型有了那校验失败的时候呢用 safeParse 拿到结构化的错误parse失败会抛ZodError但更常用的是safeParse它返回一个判别联合不用 try/catch。这段代码做的事是校验失败时把每个字段的错误逐条列出来。const result Player.safeParse({ username: 42, xp: 100 }); if (!result.success) { // 每个问题都带 path 和 message能直接定位到字段 console.log(result.error.issues); // [ // { expected: string, code: invalid_type, path: [username], message: Invalid input: expected string }, // { expected: number, code: invalid_type, path: [xp], message: Invalid input: expected number } // ] } else { const player result.data; // { username: string; xp: number } }实战一注册表单的 Zod 数据校验上面三个能力拼起来就是一个完整的表单校验器。这段代码做的事是用链式方法约束每个字段再用refine写跨字段规则包括异步检查。import * as z from zod; const RegisterForm z .object({ // 用户名3-20 位只允许字母、数字、下划线 username: z.string() .min(3, 用户名至少 3 个字符) .max(20, 用户名最多 20 个字符) .regex(/^[a-zA-Z0-9_]$/, 只能包含字母、数字和下划线), // 邮箱v4 用 .check() 挂校验项 email: z.string().check(z.email(请输入有效的邮箱)), // 密码至少 8 位且同时包含大小写和数字 password: z .string() .min(8, 密码至少 8 位) .regex(/[A-Z]/, 需要大写字母) .regex(/[a-z]/, 需要小写字母) .regex(/\d/, 需要数字), confirmPassword: z.string(), }) // 跨字段校验两次密码必须一致 .refine((d) d.password d.confirmPassword, { message: 两次输入的密码不一致, path: [confirmPassword], // 错误定位到这个字段 }) // 异步校验用户名查重 .refine(async (d) await checkUsername(d.username), { message: 用户名已被占用, path: [username], }); // 含异步 refine用 safeParseAsync const result await RegisterForm.safeParseAsync(formValues);表单里checkUsername是你自己的查重函数返回Promiseboolean即可。表单数据从前端来接口数据从外部服务来——下一段看后者。实战二给 API 入参加个守门员接口入参的校验思路和表单一样区别在于错误要按字段返回给调用方而且往往带分页、ID 这类约束。这段代码做的事是定义入参 schema校验失败时把 issues 按 path 聚合回显。import * as z from zod; // 入参 schemauuid 校验 分页参数范围 const ListUsersInput z.object({ page: z.number().int(page 必须是整数).min(1, page 最小为 1), pageSize: z.number().int().min(1).max(100, pageSize 最大 100), userId: z.string().uuid(userId 必须是合法的 UUID).optional(), }); export async function listUsers(body: unknown) { const result await ListUsersInput.safeParseAsync(body); if (!result.success) { // 把错误按字段聚合成 { 字段名: 消息 } 回显给调用方 const fieldErrors: Recordstring, string {}; for (const issue of result.error.issues) { const key issue.path.join(.) || (root); fieldErrors[key] issue.message; } return { status: 400, errors: fieldErrors }; } // 从这里开始input 就是强类型直接用 const input result.data; return { status: 200, data: await queryUsers(input) }; }到这里校验数据已经覆盖得差不多了。接下来是几个不那么显眼、但项目里很快会用到的用法。进阶错误中文化、编译提速、迷你版错误消息太英文一行配置换中文 locale问题是默认错误消息是英文直接透传给用户不体面全项目手写 message 又太累。做法给z.config挂一个 locale。效果所有没显式传 message 的校验项都会用中文报错。import * as z from zod; import zhCN from zod/locales/zh-CN; // 全局生效后续未指定 message 的错误都用中文 z.config(zhCN); const S z.object({ age: z.number().int(age 必须是整数) }); S.safeParse({ age: xx }); // issues[0].message 无效的类型预期 number实际 string仓库里 packages/zod/src/v4/locales/ 内置了 60 多个语言按文件路径按需引入没有的语言可以照模板自己写一个。热路径上把校验提速z.compile(schema)问题是每次请求都要跑一遍复杂 schema解析开销肉眼可见。做法用z.compile把 schema 提前编译成快速路径。效果官方 README 给出的 55 个 schema 基准测试里中位数提速 2.4 倍大对象或大数组场景约 9 倍。import * as z from zod; const Player z.object({ username: z.string(), xp: z.number() }); // 返回一个编译版 schema用法和原来完全一样 const FastPlayer z.compile(Player); const data FastPlayer.parse({ username: billie, xp: 100 });注意两点含异步 refine 的 schema 无法编译z.compile会原样返回不会报错从编译版 schema 再派生比如.refine()会回到未编译状态记得对最终 schema 编译。体积敏感的场景zod/mini问题是完整版带着你可能永远用不到的 APIbundle 里每一字节都是成本。做法改用zod/mini入口只保留声明和校验的骨架。效果核心 API 保持一致体积显著更小且支持 tree-shaking。import * as z from zod/mini; const User z.object({ name: z.string(), age: z.number().check(z.int(), z.positive()), }); // 迷你版的 parse 是函数式schema 和输入分开传 const data z.parse(User, { name: Colin, age: 30 });和同类库一句话的区别Joi、Valibot 这类运行时校验库也能干这些活但 Zod 是类型系统优先设计的schema 即类型来源z.infer一次搞定静态类型不用在类型定义和校验规则之间来回同步。接下来做什么两件事第一npm install zod把实战一的表单校验原样跑起来再对照 packages/zod/README.md 的 API 列表补齐你缺的字段第二打开 packages/zod/src/v4/classic/tests/ 挑几个测试文件读那里是现成的用法示例库。用法上的疑问直接去仓库提 issue 讨论比翻文档快。【免费下载链接】zodTypeScript-first schema validation with static type inference项目地址: https://gitcode.com/GitHub_Trending/zo/zod创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表