react-hook-formzodnextjs14

How to Use react-hook-form's handleSubmit with Zod Schema: Typing onSubmit Data as z.input Instead of z.output


I'm using react-hook-form with a Zod schema that transforms its input. I'm encountering a typescript issue where the handleSubmit function is passing data to my onSubmit function typed as z.input<typeof EmailOrPhoneSchema> instead of the expected z.output<typeof EmailOrPhoneSchema>.

Here's my Zod schema:

import { z } from "zod";

const phoneRegex = /^\+?[1-9]\d{1,14}$/;

const EmailOrPhoneSchema = z.object({
  emailOrPhone: z
    .string()
    .trim()
    .refine(
      (value) => {
        const isEmail = z.string().email().safeParse(value).success;
        const isPhone = phoneRegex.test(value);
        return isEmail || isPhone;
      },
      {
        message: "Invalid email or phone number",
      }
    ),
})
.transform(({ emailOrPhone }) => {
  const isEmail = z.string().email().safeParse(emailOrPhone).success;
  const isPhone = phoneRegex.test(emailOrPhone);
  return {
    isEmail,
    isPhone,
    value: isEmail ? emailOrPhone.toLowerCase() : emailOrPhone.replace(/\D/g, ""),
  };
});

export default EmailOrPhoneSchema;

The schema takes a string input but outputs an object with isEmail, isPhone, and value properties. However, when I use this schema with react-hook-form, the onSubmit function receives data typed as the input type, not the output type.

How can I ensure that react-hook-form correctly types the data passed to onSubmit as z.output<typeof EmailOrPhoneSchema> instead of z.input<typeof EmailOrPhoneSchema>?

here is my react-hook-form usage


import React from "react";
import { Label } from "../ui/label";
import { Input } from "../ui/input";
import SubmitButton from "../small/submit-button";
import { useForm } from "react-hook-form";
import { z } from "zod";
import EmailOrPhoneSchema from "@/schemas/shared/email-or-phone";
import { zodResolver } from "@hookform/resolvers/zod";

type Props = {};

export default function ForgotPasswordForm({}: Props) {
  const {
    register,
    formState: { errors },
    handleSubmit,
  } = useForm<z.input<typeof EmailOrPhoneSchema>>({
    resolver: zodResolver(EmailOrPhoneSchema),
  });

  function onSubmit(data: z.output<typeof EmailOrPhoneSchema>) {
    console.log(data);
  }
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div className="space-y-2">
        <Label htmlFor="email-phone">Email or Phone</Label>
        <Input
          {...register("emailOrPhone")}
          id="email-phone"
          type="text"
          placeholder="Enter your email or phone number"
          required
        />
        {errors.emailOrPhone && (
          <span className="text-red-500 text-xs">
            {errors.emailOrPhone.message}
          </span>
        )}
      </div>
      <SubmitButton type="submit" className="w-full mt-4">
        Reset Password
      </SubmitButton>
    </form>
  );
}

I tried logging the output data in my onSubmit function, and the actual data structure matches what I expect from the transformed schema. However, the TypeScript typing doesn't reflect this transformation.

Specifically, I expected the data in onSubmit to be typed as:

{
  isEmail: boolean;
  isPhone: boolean;
  value: string;
}

But instead, TypeScript is inferring the type as:

{
  emailOrPhone: string;
}

This mismatch between the actual data structure and the TypeScript type is causing issues in my code where I'm trying to access the transformed properties. The data itself is correct, but I'm getting TypeScript errors when trying to use it as intended.


Solution

  • I fixed this by passing more generics. Here's the fixed code:

    const {
      register,
      formState: { errors },
      handleSubmit,
    } = useForm<
      z.input<typeof EmailOrPhoneSchema>,
      unknown,
      z.output<typeof EmailOrPhoneSchema>
    >({
      resolver: zodResolver(EmailOrPhoneSchema),
    });
    

    This solution is related to the issue discussed here: handleSubmit doesn't respect Zod schema transformations in TypeScript #12050