React表单结合Zod判别式联合实现动态字段错误校验的问题
解决React Hook Form + Zod Discriminated Union动态字段的TypeScript错误问题
问题核心
当用Zod的discriminatedUnion结合React Hook Form做动态表单时,公共字段的错误提示正常,但FCL/LCL/BULK模式下的动态字段(比如f_quantity、f_weight)访问errors对象对应属性时会触发TypeScript类型错误——因为TS无法根据当前选中的mode自动收窄errors的类型,默认认为这些动态字段可能不存在。
解决方案
1. 定义各模式的错误类型
从Zod Schema生成对应模式的错误类型,为后续类型收窄做准备:
import { z } from "zod"; import { useForm, ZodResolver } from "@hookform/resolvers/zod"; // 公共字段Schema const commonSchema = z.object({ from: z.string().min(1, "From is required"), to: z.string().min(1, "To is required"), modes: z.enum(["FCL", "LCL", "BULK"]), }); // 各模式专属Schema const fclSchema = commonSchema.extend({ modes: z.literal("FCL"), f_quantity: z.number().min(1, "Quantity is required"), f_weight: z.number().min(0.1, "Weight is required"), }); const lclSchema = commonSchema.extend({ modes: z.literal("LCL"), l_volume: z.number().min(0.1, "Volume is required"), }); const bulkSchema = commonSchema.extend({ modes: z.literal("BULK"), b_totalWeight: z.number().min(1, "Total weight is required"), }); // 联合Schema const formSchema = z.discriminatedUnion("modes", [fclSchema, lclSchema, bulkSchema]); // 生成表单数据类型 type FormData = z.infer<typeof formSchema>; // 生成各模式对应的错误类型 type FclErrors = z.ZodError<z.infer<typeof fclSchema>>["formErrors"]["fieldErrors"]; type LclErrors = z.ZodError<z.infer<typeof lclSchema>>["formErrors"]["fieldErrors"]; type BulkErrors = z.ZodError<z.infer<typeof bulkSchema>>["formErrors"]["fieldErrors"];
2. 类型收窄访问动态字段错误
在渲染动态字段的条件分支里,通过类型断言或类型守卫告诉TS当前模式下对应字段的错误是存在的:
方式一:类型断言(简单直接)
const { register, handleSubmit, watch, formState: { errors } } = useForm<FormData>({ resolver: ZodResolver(formSchema), defaultValues: { from: "", to: "", modes: "FCL", }, }); const currentMode = watch("modes"); return ( <form onSubmit={handleSubmit((data) => console.log(data))}> {/* 公共字段 */} <input {...register("from")} placeholder="From" /> {errors.from?.[0] && <p>{errors.from[0]}</p>} <input {...register("to")} placeholder="To" /> {errors.to?.[0] && <p>{errors.to[0]}</p>} <select {...register("modes")}> <option value="FCL">FCL</option> <option value="LCL">LCL</option> <option value="BULK">BULK</option> </select> {/* FCL动态字段 */} {currentMode === "FCL" && ( <div> <input type="number" {...register("f_quantity")} placeholder="FCL Quantity" /> {(errors as FclErrors).f_quantity?.[0] && <p>{(errors as FclErrors).f_quantity[0]}</p>} <input type="number" {...register("f_weight")} placeholder="FCL Weight" /> {(errors as FclErrors).f_weight?.[0] && <p>{(errors as FclErrors).f_weight[0]}</p>} </div> )} {/* LCL动态字段 */} {currentMode === "LCL" && ( <div> <input type="number" {...register("l_volume")} placeholder="LCL Volume" /> {(errors as LclErrors).l_volume?.[0] && <p>{(errors as LclErrors).l_volume[0]}</p>} </div> )} {/* BULK动态字段 */} {currentMode === "BULK" && ( <div> <input type="number" {...register("b_totalWeight")} placeholder="Bulk Total Weight" /> {(errors as BulkErrors).b_totalWeight?.[0] && <p>{(errors as BulkErrors).b_totalWeight[0]}</p>} </div> )} <button type="submit">Submit</button> </form> );
方式二:类型守卫(更优雅)
先定义类型守卫函数,让TS自动推断错误类型:
function isFclError(errors: typeof errors): errors is FclErrors { return currentMode === "FCL"; } function isLclError(errors: typeof errors): errors is LclErrors { return currentMode === "LCL"; } function isBulkError(errors: typeof errors): errors is BulkErrors { return currentMode === "BULK"; }
使用时无需手动断言:
{currentMode === "FCL" && ( <div> <input type="number" {...register("f_quantity")} placeholder="FCL Quantity" /> {isFclError(errors) && errors.f_quantity?.[0] && <p>{errors.f_quantity[0]}</p>} </div> )}
原理说明
Zod的discriminatedUnion生成的是联合类型的表单数据,但React Hook Form返回的errors类型是所有可能字段的错误联合类型。通过类型断言或守卫,我们可以根据当前选中的mode,将errors收窄为对应模式的错误类型,让TS认可动态字段的存在。
内容的提问来源于stack exchange,提问作者Amith A G
相关产品推荐
相关产品推荐

