使用react-hook-form+zod判别联合时,FieldErrors无name属性报错
React Hook Form + Zod 判别联合字段TypeScript错误解决
问题场景
用React Hook Form结合Zod做表单验证时,判别联合(discriminatedUnion)分支里的name、taxId等字段,无法通过errors对象正常访问,TypeScript提示属性不存在,但公共字段(如phone)能被正确识别。
报错信息
Property 'name' does not exist on type 'FieldErrors<{ phone: string; email: string; city: string; district: string; address: string; paymentMethod: "paypal" | "bank"; customerType: "individual"; name: string; code?: string | undefined; notes?: string | undefined; } | { ...; }>'.
Property 'name' does not exist on type 'Partial<FieldErrorsImpl<{ phone: string; email: string; city: string; district: string; address: string; paymentMethod: NonNullable<"paypal" | "bank">; customerType: "corporate"; taxId: string; ... 4 more ...; notes: string; }>> & { ...; }'.ts(2339)
原代码片段
const commonSchema = z.object({ phone: z .string() .regex(/^\+?[1-9]\d{1,14}$/, "Geçerli bir telefon numarası giriniz") .nonempty("Telefon zorunludur"), email: z .string() .email("Geçerli bir email giriniz") .nonempty("Email zorunludur"), city: z.string().nonempty("Lütfen il seçiniz"), district: z.string().nonempty("Lütfen ilçe seçiniz"), address: z.string().nonempty("Adres zorunludur"), paymentMethod: z.enum(["paypal", "bank"]), code: z.string().optional(), notes: z.string().optional(), }); const individualSchema = z.object({ customerType: z.literal("individual"), name: z.string().nonempty("Ad Soyad zorunludur"), }); const corporateSchema = z.object({ customerType: z.literal("corporate"), taxId: z.string().nonempty("Vergi Numarası zorunludur"), companyName: z.string().nonempty("Şirket Adı zorunludur"), taxOffice: z.string().nonempty("Vergi Dairesi zorunludur"), billingAddress: z.string().nonempty("Fatura Adresi zorunludur"), }); const schema = z .discriminatedUnion("customerType", [individualSchema, corporateSchema]) .and(commonSchema); export type PaymentForm = z.infer<typeof schema>; export default function PaymentForm() { const { control, handleSubmit, watch, formState: { errors }, } = useForm<PaymentForm>({ resolver: zodResolver(schema), defaultValues: { customerType: "individual", paymentMethod: "paypal", }, }); const customerType = watch("customerType"); return ( <> {customerType === "individual" && ( <Controller name="name" control={control} render={({ field }) => ( <TextField {...field} label="Ad Soyad*" variant="outlined" fullWidth error={!!errors.name} // 此处报错 helperText={errors.name?.message || " "} // 此处报错 /> )} /> )} </> ); }
解决方案
方法1:重构Zod Schema(推荐)
将公共字段直接扩展到每个分支的Schema中,替代.and()方法,让TypeScript能清晰识别每个分支的类型,自动推断对应错误字段:
// 定义公共字段 const commonFields = z.object({ phone: z .string() .regex(/^\+?[1-9]\d{1,14}$/, "Geçerli bir telefon numarası giriniz") .nonempty("Telefon zorunludur"), email: z .string() .email("Geçerli bir email giriniz") .nonempty("Email zorunludur"), city: z.string().nonempty("Lütfen il seçiniz"), district: z.string().nonempty("Lütfen ilçe seçiniz"), address: z.string().nonempty("Adres zorunludur"), paymentMethod: z.enum(["paypal", "bank"]), code: z.string().optional(), notes: z.string().optional(), }); // 扩展公共字段到个人用户Schema const individualSchema = commonFields.extend({ customerType: z.literal("individual"), name: z.string().nonempty("Ad Soyad zorunludur"), }); // 扩展公共字段到企业用户Schema const corporateSchema = commonFields.extend({ customerType: z.literal("corporate"), taxId: z.string().nonempty("Vergi Numarası zorunludur"), companyName: z.string().nonempty("Şirket Adı zorunludur"), taxOffice: z.string().nonempty("Vergi Dairesi zorunludur"), billingAddress: z.string().nonempty("Fatura Adresi zorunludur"), }); // 创建判别联合Schema const schema = z.discriminatedUnion("customerType", [individualSchema, corporateSchema]); export type PaymentForm = z.infer<typeof schema>;
修改后,当customerType === "individual"时,TypeScript会自动窄化errors的类型,直接访问errors.name不会报错,且能获得正确的智能提示。
方法2:类型断言(快速临时解决)
如果不想修改Schema,可通过类型断言告诉TypeScript当前分支对应的错误类型:
{customerType === "individual" && ( <Controller name="name" control={control} render={({ field }) => { // 断言errors为个人用户分支的错误类型 const individualErrors = errors as FieldErrors<Extract<PaymentForm, { customerType: "individual" }>>; return ( <TextField {...field} label="Ad Soyad*" variant="outlined" fullWidth error={!!individualErrors.name} helperText={individualErrors.name?.message || " "} /> ); }} /> )}
方法3:自定义类型守卫
封装类型守卫函数,让类型判断更严谨:
// 定义类型守卫函数 function isIndividualErrors(errors: FieldErrors<PaymentForm>): errors is FieldErrors<Extract<PaymentForm, { customerType: "individual" }>> { // 因为已经通过customerType判断分支,直接返回true即可 return true; }
然后在组件中使用:
{customerType === "individual" && ( <Controller name="name" control={control} render={({ field }) => { const individualErrors = isIndividualErrors(errors) ? errors : {} as FieldErrors<Extract<PaymentForm, { customerType: "individual" }>>; return ( <TextField {...field} label="Ad Soyad*" variant="outlined" fullWidth error={!!individualErrors.name} helperText={individualErrors.name?.message || " "} /> ); }} /> )}
问题原因
原代码中使用.and()将判别联合Schema与公共字段Schema合并,导致TypeScript无法正确解析联合类型的分支结构,React Hook Form也就无法推断出每个分支对应的错误字段。将公共字段直接扩展到分支Schema后,判别联合的类型结构更清晰,TypeScript能准确识别每个分支的字段及对应错误。
内容的提问来源于stack exchange,提问作者Librouse
相关产品推荐
相关产品推荐

