如何构建Zod Schema:基于已有字段动态设置必填项
问题背景
有如下TypeScript接口:
interface Purchase { invoiceNo?: string purchaseDate?: string placeOfPurchase?: string productId: string serialId?: string purchasePrice?: number }
编辑后端记录时,要求已存在值的字段必须设为必填,不存在值的字段可忽略。比如某记录包含productId、serialId、purchaseDate三个字段,校验时需强制这三个字段的类型与必填性,其余字段不做要求。当前实现繁琐,需要更简洁、可复用的解决方案。
优化方案1:基于Zod原生能力动态生成Schema
核心利用Zod的partial()和required()方法,结合实体数据动态生成校验规则,类型安全且贴合Zod设计理念。
步骤1:定义基础Schema
先维护所有字段的基础校验规则,原本必填的productId保持必填,其余字段先设为可选:
import { z } from "zod"; const basePurchaseSchema = z.object({ invoiceNo: z.string().trim().min(1, { message: 'Invoice No is required' }).optional(), purchaseDate: z.string().trim().min(1, { message: 'Purchase Date is required' }).optional(), placeOfPurchase: z.string().trim().min(1, { message: 'Place of Purchase is required' }).optional(), productId: z.string().trim().min(1, { message: 'Product is required' }), serialId: z.string().trim().min(1, { message: 'Serial Number is required' }).optional(), purchasePrice: z.number().min(1, { message: 'Purchase Price is required' }).optional(), }).strict();
步骤2:编写通用动态Schema生成函数
一个函数搞定字段筛选和Schema生成,支持复用在其他实体场景:
import type { ZodObject, ZodRawShape } from "zod"; function getDynamicRequiredSchema<Schema extends ZodObject<ZodRawShape>>( baseSchema: Schema, entity: Partial<z.infer<Schema>> ) { // 提取实体中存在有效值的字段(排除undefined/null) const existingKeys = Object.entries(entity) .filter(([_, value]) => value !== undefined && value !== null) .map(([key]) => key) as Array<keyof z.infer<Schema>>; // 先将所有字段设为可选,再把存在值的字段和原本必填的字段设为必填 return baseSchema.partial().required({ ...Object.fromEntries(existingKeys.map(key => [key, true])), productId: true, }); }
步骤3:结合react-hook-form使用
直接传入动态生成的Schema,默认值无需额外处理:
import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; // 示例:从后端获取的现有记录 const purchase: Purchase = { productId: "PROD-123", serialId: "SER-456", purchaseDate: "2024-05-20" }; const dynamicSchema = getDynamicRequiredSchema(basePurchaseSchema, purchase); const { register, handleSubmit, formState: { errors } } = useForm({ resolver: zodResolver(dynamicSchema), defaultValues: purchase, // 直接传入实体,react-hook-form会自动忽略不存在的字段 });
优化方案2:精简现有工具函数
如果不想重构基础Schema,可以合并原有两个工具函数的逻辑,减少冗余:
import { z } from "zod"; import type { ZodObject } from "zod"; import { get } from "lodash"; // 合并Schema筛选和默认值生成逻辑 function getActiveFormConfig<S extends ZodObject<any>, E>(schema: S, entity: E) { const activeKeys = Object.keys(schema.shape).filter(key => get(entity, key)); return { activeSchema: schema.pick(Object.fromEntries(activeKeys.map(k => [k, true]))), defaultValues: Object.fromEntries(activeKeys.map(k => [k, get(entity, k)])) as Partial<E> }; } // 使用方式 const { activeSchema, defaultValues } = getActiveFormConfig(schema, purchase); const { register, handleSubmit, formState: { errors } } = useForm({ resolver: zodResolver(activeSchema), defaultValues, });
方案优势
- 方案1:类型安全性更强,无需手动维护字段映射,新增字段只需在
basePurchaseSchema中添加,复用性更高。 - 方案2:改动小,适合基于现有代码快速优化,减少重复逻辑。
内容的提问来源于stack exchange,提问作者Hard-Boiled Wonderland
相关产品推荐
相关产品推荐

