当前位置: 首页 > news >正文

Zod:TypeScript 类型守卫与数据验证

我见过许多因为运行时数据不匹配而导致的崩溃,也曾写过无数防御性代码和 any 断言,哈哈 😄。TypeScript 的类型安全本来就不该止步于编译期。直到遇见 Zod,Zod 不仅是一个验证库,它为 TypeScript 带来运行时安全,是目前最优雅、最彻底的解决方案。

我们为何需要 Zod?

TypeScript 最让人上瘾的地方在于编译时类型检查,但这也是它的最大谎言,因为类型在运行时彻底消失,你需要小心小心再小心,使用 TypeScript 并不代表类型安全。

interface User {name: string;age: number;role: 'admin' | 'user';
}fetch('/api/user').then(res => res.json()).then((data: User) => {// 编译通过,但如果 data.role 返回的是 "administrator",运行会报错console.log(data.role.toUpperCase());
});

而 Zod 的答案是:只定义一次 Schema,既得到运行时验证,又得到完美的 TypeScript 类型。

const UserSchema = z.object({name: z.string(),age: z.number(),role: z.enum(['admin', 'user']),
});type User = z.infer<typeof UserSchema>;

通过 Schema 推断出完美类型 User,无需二次声明。这样, UserSchema 用于运行时验证,User 用于类型推断。

Zod 入门

npm install zod
import { z } from 'zod';// 基础类型
const StringSchema = z.string();
const NumberSchema = z.number().int().positive();

核心 API:parse 和 safeParse,我建议优先使用 .safeParse()

const schema = z.string();try {const result = schema.parse(123);console.log(result);
} catch (error) {console.error('验证失败:', error.errors);
}

parse 抛出 ZodError, 你需要通过 try catch 捕获错误,否则导致程序崩溃。

const schema = z.string();
const result = schema.safeParse(123);if (result.success) {console.log('验证成功:', result.data);
} else {console.log('验证失败:', result.error.errors);
}

safeParse 返回 { success: true, data: T }{ success: false, error: ZodError },你可以通过 success 判断是否验证成功,然后通过 data 获取验证后的数据,或者通过 error.errors 获取错误信息, 这样你可以优雅地处理错误。

构建复杂数据模型

const AddressSchema = z.object({street: z.string(),city: z.string(),zipCode: z.string().regex(/^\d{5}$/),
});const UserSchema = z.object({id: z.string().uuid(),name: z.string().min(2).max(50),email: z.string().email(),age: z.number().int().min(13).max(120),address: AddressSchema.optional(), // 可选嵌套对象tags: z.array(z.string()).default([]), // 默认值role: z.enum(['admin', 'user', 'moderator']),status: z.enum(['active', 'inactive']).default('active'),
});

组合技巧,这些是我最常用的:

.extend() 是 Zod 最被低估的特性之一。它让你能以面向对象的方式构建 Schema 体系,比 interface 继承更安全,因为运行时验证也会继承。

// 扩展
const AdminSchema = UserSchema.extend({permissions: z.array(z.string()),
});

比较常见的是,实现查询接口的分页查询 Schema,分页查询 Schema 包含 page 和 pageSize 字段,自其他 Schema 可以继承分页查询 Schema 并添加其他字段。

const PageSchema = z.object({page: z.number().min(1).default(1),pageSize: z.number().min(1).max(100).default(10),
});const UserPageSchema = PageSchema.extend({name: z.string().min(2).max(50),
});type UserPage = z.infer<typeof UserPageSchema>;

合并, 优先级后者覆盖前者。

const MergedSchema = UserSchema.merge(z.object({role: z.literal('admin'), // 强制覆盖
}));

交集。

const IntersectionSchema = z.intersection(UserSchema, z.object({isVerified: z.boolean(),
}));

进阶模式与精细校验

可辨识联合(Discriminated Union),比如我们用它来处理 Redux Action。

可辨识联合(Discriminated Union),也称为标签联合(Tagged Union)或代数数据类型(Algebraic Data Type),是一种高级类型系统特性,用于表示可能是多种不同类型之一的值。

const ActionSchema = z.discriminatedUnion('type', [z.object({ type: z.literal('INCREMENT'), payload: z.number() }),z.object({ type: z.literal('DECREMENT'), payload: z.number() }),z.object({ type: z.literal('SET_USER'), payload: UserSchema }),
]);type Action = z.infer<typeof ActionSchema>;

z.discriminatedUnion 比手动写 .or() 更清晰,TypeScript 窄化(narrowing)也更完美。

字符串高级校验

z.string().min(8, '至少8位').regex(/[A-Z]/, '必须含大写字母').regex(/[a-z]/, '必须含小写字母').regex(/[0-9]/, '必须含数字').regex(/[^A-Za-z0-9]/, '必须含特殊字符');

通过 Transform 验证后自动转换,这太棒了,比如写 restful 接口时,我们希望 Query id 是 number 类型,但是传入 string 类型,我们可以借助 Transform 自动转换为 number 类型。

const IdSchema = z.string().transform(str => parseInt(str));

推断 TypeScript 类型

修改 Schema,类型自动更新,无需手动更新类型,完美同步。

type User = z.infer<typeof UserSchema>;

与 TypeScript 原生 enum 互操作。

enum Role {Admin = 'admin',User = 'user',
}const RoleSchema = z.nativeEnum(Role);

常见应用场景

API 响应验证

async function apiFetch<T extends z.ZodType>(url: string,schema: T
): Promise<z.infer<T>> {const res = await fetch(url);const data = await res.json();const result = schema.safeParse(data);if (!result.success) {throw new Error(`API验证失败: ${result.error.message}`);}return result.data;
}// 使用
const user = await apiFetch('/api/user', UserSchema);

表单验证

与 React-Hook-Form 完美结合,类型安全 + 错误信息自动同步。AI 非常喜欢这套方案,AI 生成的代码非常不容易出错。

const formSchema = z.object({email: z.string().email('邮箱格式错误'),password: z.string().min(8, '密码至少8位'),confirm: z.string(),
}).refine(data => data.password === data.confirm, {message: "两次密码不一致",path: ["confirm"],
});type FormData = z.infer<typeof formSchema>;const { register, handleSubmit, formState: { errors } } = useForm<FormData>({resolver: zodResolver(formSchema),
});

但是,我们最常用的 antd 的 Form 组件,与 Zod 结合并不完美 #40580。表单一旦复杂,字段之间的关联性就更多了,如果都在 jsx 中处理,代码的可读性、可维护性就大大降低,如果把字段的定义以及验证单独提取出来,形成业务实体对应的实体逻辑,无疑更好。

社区有人为此实现了一个 antd-zod 库,是目前我比较推荐的方案。

import { createSchemaFieldRule } from 'antd-zod';const CustomFormValidationSchema = z.object({fieldString: z.string(),fieldNumber: z.number(),
});const rule = createSchemaFieldRule(CustomFormValidationSchema);export function SimpleForm() {return (<Form><Form.Item label="String field" name="fieldString" rules={[rule]}><Input/></Form.Item><Form.Item label="Number field" name="fieldNumber" rules={[rule]}><InputNumber/></Form.Item><Button htmlType="submit">Submit</Button></Form>);
};

环境变量验证

const envSchema = z.object({DATABASE_URL: z.string().url(),NODE_ENV: z.enum(['development', 'production', 'test']),JWT_SECRET: z.string().min(32),
});type Env = z.infer<typeof envSchema>;export const env = envSchema.parse(process.env);

对于环境变量校验我推荐你使用 t3-env,使用无效的环境变量部署应用程序是一件麻烦事。这个包可以帮助你避免这种情况。它支持使用任何 Standard Schema 兼容验证器,当然包括 Zod。

定义环境变量:

// src/env.mjs
import { createEnv } from "@t3-oss/env-nextjs"; // or core package
import { z } from "zod";export const env = createEnv({/** Serverside Environment variables, not available on the client.* Will throw if you access these variables on the client.*/server: {DATABASE_URL: z.string().url(),OPEN_AI_API_KEY: z.string().min(1),},/** Environment variables available on the client (and server).** 💡 You'll get type errors if these are not prefixed with NEXT_PUBLIC_.*/client: {NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().min(1),},/** Due to how Next.js bundles environment variables on Edge and Client,* we need to manually destructure them to make sure all are included in bundle.** 💡 You'll get type errors if not all variables from `server` & `client` are included here.*/runtimeEnv: {DATABASE_URL: process.env.DATABASE_URL,OPEN_AI_API_KEY: process.env.OPEN_AI_API_KEY,NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY:process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,},
});

使用时具有自动完成和类型推断功能

import { env } from "../env.mjs";export const GET = (req: Request) => {const DATABASE_URL = env.DATABASE_URL;// use it...
};

image

服务端应用:tRPC 示例

tRPC 是一个端到端类型安全的服务端框架。tRPC 的核心魔力就在于 Zod。输入验证、类型推断、自动生成客户端类型,全程零配置。

服务端实现文章发布接口,使用 Zod 验证输入

image

前端类型推断

image

端到端类型安全对于 AI 自动补全和提升生成代码质量也是非常有帮助的

image

AI 生成结构化数据

许多语言模型都能够生成结构化数据,通常定义为使用“JSON modes”或“tools”。然而,您需要手动提供模式,然后验证生成的数据,因为 LLM 可能会产生错误或不完整的结构化数据。

Vercel AI SDK 通过在 generateText 上使用 output 属性,标准化了模型提供商之间的结构化对象生成。 和 streamText。您可以使用 Zod schemas,Valibot 或 JSON schemas 来指定您想要的数据结构,AI 模型将生成符合该结构的数据。

使用 generateTextOutput.object() 从提示中生成结构化数据。该模式还用于验证生成的数据,确保类型安全和正确性。

import { generateText, Output } from 'ai';
import { deepseek } from "@ai-sdk/deepseek";
import { z } from 'zod';const { output } = await generateText({model: deepseek("deepseek-v3.1"),output: Output.object({schema: z.object({recipe: z.object({name: z.string(),ingredients: z.array(z.object({ name: z.string(), amount: z.string() }),),steps: z.array(z.string()),}),}),}),prompt: '生成一份扬州炒饭食谱',
});

image

与其他验证库对比

TypeScript 支持 类型推断 体积 学习成本 生态 推荐场景
Zod 原生一流 完美 ~6KB 极强 所有 TS 项目(强烈推荐)
Yup 需 cast ~20KB 老项目、JS 项目
Joi Node.js 后端
io-ts 好(FP 风格) 喜欢函数式编程的团队
AJV 小(快) 纯 JSON 验证场景

总结

这才是 TypeScript 应该有的样子。

http://www.jsqmd.com/news/192122/

相关文章:

  • LangGraph 记忆存储的三重境界
  • HeyGem系统依赖Python环境吗?底层框架揭秘
  • Teledyne LeCroy 力科 ZS1000 有源高阻抗电压探头
  • 机器人也怕疼!港城突破性电子皮肤:主动痛觉 + 损伤自检双buff拉满
  • 螨虫最有效的方法?卧室除螨虫最有效方法?螨虫重灾区的清洁技巧,除螨剂哪个品牌最好最实用?仙贝宁纯植物更安心 - 博客万
  • 日期时间数据的格式化与解析
  • 【EMG肌电信号】基于DWT和EMD技术去噪肌电图信号的性能研究附Matlab代码
  • C#网络通信数据压缩技术全解析(节省70%带宽的秘技)
  • 无人机巡检系统 - 智慧交通基础设施监测 - 小目标/密集目标检测(如裂缝、垃圾) - 多类别路面病害联合检测 智慧交通高清无人机视角高速路面损害检测数据集
  • C#不安全代码深度解析:如何安全实现指针与引用类型转换
  • 人才精准筛选怎么做?智能招聘系统的 AI 技术应用全解析
  • 京东关键词搜索商品列表的Python爬虫实战
  • 权限数据泄露风险预警,C#系统中你必须掌握的4大加密防护技术
  • 性能提升300%的秘密,C#拦截器在微服务通信中的实战优化方案
  • 分享7个降AI率提示词和中英文降AI工具,助你高效降AIGC率! - 殷念写论文
  • C#列表与集合表达式的完美结合(性能优化实战案例)
  • 别再手动查日志了!4个工具让C#跨平台分析效率翻倍
  • 玉溪婚纱摄影推荐:定格滇中浪漫,乐派诗登顶品质榜 - charlieruizvin
  • 玉溪婚纱摄影首选乐派诗:原创质感与星级服务的双重标杆 - charlieruizvin
  • 权威推荐!TOP6CRM解决方案深度解析:从拓客到复购的全生命周期适配 - 毛毛鱼的夏天
  • 【C#跨平台调试日志实战指南】:掌握高效日志策略,提升开发效率
  • C#异步通信模型详解:大幅提升数据吞吐量的5个关键技术点
  • 解锁本科论文写作新姿势:书匠策AI,你的学术隐形导航仪
  • HeyGem数字人系统启动脚本start_app.sh执行失败怎么办?
  • tomato代码随笔
  • 使用HeyGem批量生成数字人教学视频的完整流程解析
  • 2025年北京靠谱体育设施工程公司排行榜,奥帆体育设施工程与同行相比优势在哪 - 工业品网
  • C#跨平台权限继承实战(从Windows到Linux的无缝迁移方案)
  • B站视频图文联动:UP主可制作HeyGem操作演示系列
  • 2025高压真空负荷开关供应商TOP5权威推荐:甄选优质生产工厂与批发渠道 - myqiye