reactjstypescriptreact-hook-formdefault-valuezod

typescript for react-hook-form without default values


We have react-hook-form + zod + react-query.

How do I type values with TypeScript to let it catch a bug if I do not provide optional default values?

https://codesandbox.io/p/sandbox/react-form-form-question-hwvq7y

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useQuery } from "@tanstack/react-query";

function App() {
  const { data: formValuesFromApi } = useQuery({
    queryKey: ["query key"],
    queryFn: async () => {
      await new Promise((resolve) => {
        setTimeout(() => resolve("done"), 1000);
      });

      return {
        name: "John",
      };
    },
  });

  const schema = z.object({
    name: z.string(),
  });

  type FormValues = z.infer<typeof schema>;

  const methods = useForm<FormValues>({
    resolver: zodResolver(schema),
    values: formValuesFromApi,
    defaultValues: {
      // name: "", // NO DEFAULT VALUE
    },
  });

  const onSubmit = (data: FormValues) => {
    console.log(data);
  };

  const formValues = methods.getValues();

  // ERROR! 
  // TS thinks that type of formValues.name is string 
  const nameChars = formValues.name.split(""); 

  console.log("nameChars", nameChars);

  return (
    <form onSubmit={methods.handleSubmit(onSubmit)}>
      <input {...methods.register("name")} />
      <input type="submit" />
    </form>
  );
}

export default App;

I tried to google to find the way how to properly type such scenario.


Solution

  • The type of defaultValues should match the generic type you pass to useForm. In fact, defaultValues is used as your form data type if you don't pass in a generic type.

    const form = useForm({
      defaultValues: {
        a: 1
      }
    })
    
    // mouse over `useForm` here to see this type:
    //   useForm<{ a: number; }, any, undefined>(props?: Partial<{ ...
    

    Note how { a: number; }. is the generic type where you are trying to pass in FormValues.


    What I typically do is break the default values into their own function, which is really nice when the form gets a lots of fields.

    function getFormDefaults(): FormValues {
      return {
        name: ''
      }
    }
    

    (Or make this a hook with useMemo if making that object is expensive somehow)

    This also scales well to when you want this form to handle both creations and updates:

    function getFormDefaults(person?: Person | null): FormValues {
      return {
        name: person?.name ?? ''
      }
    }
    

    And then the useForm call looks like:

      const methods = useForm({
        resolver: zodResolver(schema),
        values: formValuesFromApi,
        defaultValues: getFormDefaults(),
      });
    

    This is nice because getFormDefaults has the return type for your form data, and typescript will yell at you if the defaults values don't conform to that. And useForm will pickup that type and then all your fields will be the correct type as well.

    See playground