Marvin
Marvin

Reputation: 133

Typescript React Hook Form error handling with zod union schema

I want to make a dynamic Form that show different input fields based on a Boolean value.

When calling useForm of the react-hook-form package I use a zod object to validate my inputs. This object is a union of two other zod objects (one for each Boolean case). This results in the error type only showing the shared options in TypeScript.

When running the code everything works and the errors show up. But I still don't like that type safety isn't provided anymore. Is there a fix to this problem?

import { zodResolver } from "@hookform/resolvers/zod"
import React from "react"
import { useForm } from "react-hook-form"
import { z } from "zod"

const TrueSchema = z.object({
    isTrue: z.literal("true"),
    trueInput: z.string().min(1, "Required, true"),
})

const FalseSchema = z.object({
    isTrue: z.literal("false"),
    falseInput: z.string().min(1, "Required, false"),
})

const FormSchema = z.discriminatedUnion("isTrue", [TrueSchema, FalseSchema])
type FormSchemaType = z.infer<typeof FormSchema>

function Example() {
    const {
        register,
        watch,
        handleSubmit,
        formState: { errors },
    } = useForm<FormSchemaType>({
        resolver: zodResolver(FormSchema),
        defaultValues: {
            isTrue: "false",
        },
    })
    const onSubmit = handleSubmit((data) => console.log("data", data))

    return (
        <form onSubmit={onSubmit}>
            <label htmlFor="radioTrue">
                <input
                    type="radio"
                    id="radioTrue"
                    value={"true"}
                    {...register("isTrue")}
                />
                True
            </label>
            <label htmlFor="radioFalse">
                <input
                    type="radio"
                    id="radioFalse"
                    value={"false"}
                    {...register("isTrue")}
                />
                False
            </label>
            {watch("isTrue") === "true" && (
                <>
                    <input
                        type="text"
                        placeholder="Bool is true"
                        {...register("trueInput")}
                    />
                    {errors.trueInput && <p>{errors.trueInput.message}</p>} {/* Only error.isTrue available here */}
                </>
            )}
            {watch("isTrue") === "false" && (
                <>
                    <input
                        type="text"
                        placeholder="Bool is false"
                        {...register("falseInput")}
                    />
                    {errors.falseInput && <p>{errors.falseInput.message}</p>} {/* Only error.isTrue available here */}
                </>
            )}
            <input type="submit" />
        </form>
    )
}

export default Example

The packages I use:

"@hookform/resolvers": "^2.9.10",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.38.0",
"zod": "^3.19.1"

Upvotes: 6

Views: 5883

Answers (1)

Jander Silva
Jander Silva

Reputation: 121

This is a limitation from the current design of React Hook Form. You can see the discussions about it, here:

For now, workarounds are needed, enforcing the right type:

import { FieldErrors, useForm } from 'react-hook-form'

const {
  formState: { errors },
} = useForm<UnionSchema>()

<Component>
  {(() => {
    if (condition) {
      const formErrors: FieldErrors<TheRightSchema> = errors
      // formErrors knows the right fields.
      return (
        <>
          // Your inputs
        </>
      )
    }
  })()}
</Component>

or

"email" in errors && errors.email

Upvotes: 5

Related Questions