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.
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.