You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 15:34:53