resolvers icon indicating copy to clipboard operation
resolvers copied to clipboard

v5.0: Schema with .default fields result in inaccurate typing

Open eikowagenknecht opened this issue 1 year ago • 21 comments

Describe the bug With the new v5.0.0, I got a type error:

const FormSchema = z.object({
  debug_mode: z.boolean().default(true),
});
type FormValues = z.infer<typeof FormSchema>;
  const form = useForm<FormValues>({
    resolver: zodResolver(FormSchema),
    defaultValues: {
      debug_mode: true,
    },
  });

The automatic resolution gives me:

const form: UseFormReturn<{
    debug_mode?: boolean | undefined;
}

I think this is incorrect as the schema does not have any optional members.

When I remove the .default() in the schema, so it looks like this:

const FormSchema = z.object({
  debug_mode: z.boolean(),
});

the resolution changes to:

const form: UseFormReturn<{
    debug_mode: boolean;
}

which is consistent with type FormValues = z.infer<typeof FormSchema>; again.

Codesandbox link (Required)

As this is a typing issue and Code Sandbox uses JS, it's not really applicable.

eikowagenknecht avatar Apr 01 '25 10:04 eikowagenknecht

Hey! What's the type error you're seeing? Also, any chance you could throw up a quick CodeSandbox so we can take a look?

jorisre avatar Apr 01 '25 10:04 jorisre

Type 'Resolver<{ debug_mode?: boolean | undefined; }, any, { debug_mode: boolean; }>' is not assignable to type 'Resolver<{ debug_mode: boolean; }, any, { debug_mode: boolean; }>'.
  Types of parameters 'options' and 'options' are incompatible.
    Type 'ResolverOptions<{ debug_mode: boolean; }>' is not assignable to type 'ResolverOptions<{ debug_mode?: boolean | undefined; }>' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
      Type 'boolean | undefined' is not assignable to type 'boolean'.
        Type 'undefined' is not assignable to type 'boolean'.ts(2322)

Without exactOptionalPropertyTypes it changes to:

Type 'Resolver<{ debug_mode?: boolean | undefined; }, any, { debug_mode: boolean; }>' is not assignable to type 'Resolver<{ debug_mode: boolean; }, any, { debug_mode: boolean; }>'.
  Types of parameters 'options' and 'options' are incompatible.
    Type 'ResolverOptions<{ debug_mode: boolean; }>' is not assignable to type 'ResolverOptions<{ debug_mode?: boolean | undefined; }>'.
      Type 'boolean | undefined' is not assignable to type 'boolean'.
        Type 'undefined' is not assignable to type 'boolean'.ts(2322)

Unfortunately I couldn't get the code sandbox to properly work with TS instead of JS, but it should be reproducible fairly easily with the snippets above.

eikowagenknecht avatar Apr 01 '25 10:04 eikowagenknecht

Same here: https://github.com/react-hook-form/resolvers/issues/759

jonathanwilke avatar Apr 01 '25 10:04 jonathanwilke

Just wondering, why are you manually typing useForm<FormValues>? V5's supposed to handle that automatically. If you do need to type it, here is the way to go.

type FormValues = z.input<typeof FormSchema>; // Notice the z.input

z.infer and z.output are the same. I'll clarify this in the release notes.

The TypeScript syntax for useForm is: useForm<Input, Context, Output>.

jorisre avatar Apr 01 '25 10:04 jorisre

The manual typing was a leftover of the previous version. Like @jonathanwilke, I did not expect a breaking change that was not mentioned in the release notes.

But:

Even removing the manual typing, when .default() is used in the schema definition, this leads to errors further down the line because this:

const FormSchema = z.object({
  debug_mode: z.boolean().default(true),
});

in automatic mode still resolves to this:

const form: UseFormReturn<{
    debug_mode?: boolean | undefined;
}

This leads to problems later on:

                      <FormControl>
                        <Switch
                          checked={field.value}
                          onCheckedChange={field.onChange}
                        />
                      </FormControl>

Now checked can be true, false or undefined, which is not accepted by the component I use, leading to a typing error there:

Type '{ checked: boolean | undefined; onCheckedChange: (...event: any[]) => void; }' is not assignable to type 'Omit<SwitchProps & RefAttributes<HTMLButtonElement>, "ref">' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
  Types of property 'checked' are incompatible.
    Type 'boolean | undefined' is not assignable to type 'boolean'.
      Type 'undefined' is not assignable to type 'boolean'.ts(2375)

Without .default() it resolves to boolean only. I think adding a default value in the schema should not make the result undefined.

eikowagenknecht avatar Apr 01 '25 12:04 eikowagenknecht

Hmm, I understand what you're saying. Resolvers are simply functions that connect react-hook-form with validation libraries.

When you use zod's .default() and define a schema like this:

const schema = z.object({
  debug_mode: z.boolean().default(true),
});

type Schema = z.input<typeof schema>; // { debug_mode?: boolean | undefined; }

The Schema type becomes { debug_mode?: boolean | undefined; }. That's why the resolver returns this type.

Based on Zod's tests, this behavior appears to be intentional.

jorisre avatar Apr 01 '25 13:04 jorisre

The test makes sense when I think about it. If I put something in when a default is defined, I can use an input of undefined and it will still work, because it has the default.

The output however will never contain something undefined (because the default is there as fallback).

So the resulting type should maybe match the output and not the input? z.infer instead of z.input?

eikowagenknecht avatar Apr 01 '25 13:04 eikowagenknecht

I think here is the type contradiction, cc @kotarella1110 @jorisre

const FormSchema = z.object({
  debug_mode: z.boolean().default(true),  // 🔎 which turn this field to be optional debug_mode?: boolean | undefined;
});

type FormValues = z.infer<typeof FormSchema>; // 🔎 infer type which debug_mode: boolean

const form = useForm<FormValues>({
  resolver: zodResolver(FormSchema),
  // ✅ in this case, the infer contradicts with <FormValues>
  defaultValues: {
    debug_mode: true,
  },
});

bluebill1049 avatar Apr 03 '25 09:04 bluebill1049

I am having issue with this as well. Its making it so that I cant pass the result from calling useFieldArray as a prop in a good way.

https://codesandbox.io/p/devbox/brave-resonance-6dkqyc?file=%2Fsrc%2FApp.tsx%3A27%2C20&workspaceId=ws_4FZNYNsk5KKEpTzN4z6yqN

The issue resolves by removing the .default([])

FredrikMWold avatar Apr 03 '25 12:04 FredrikMWold

I'm also having this issue

ic-768 avatar Apr 04 '25 09:04 ic-768

fix waiting room

0-don avatar Apr 04 '25 13:04 0-don

I had to explicit use input + output to make it work without errors:

  type FormValues = z.infer<typeof schema>; // same as z.output

  const form = useForm<z.input<typeof schema>, any, FormValues>({
    resolver: zodResolver(schema),
    defaultValues: {
...

maiconcarraro avatar Apr 09 '25 19:04 maiconcarraro

I think it's more than just a typing issue. And I think it's more than just an issue with the resolver.

I'm currently using:

  • "react-hook-form": "^7.53.1"
  • "@hookform/resolvers": "^3.9.1"

When I upgrade just react-hook-form (not the resolvers) ^7.55.0... I'm failing validations on all boolean fields that have a default value set to false.

I'm currently using "zod": "^3.24.2"

Reverting back to "react-hook-form": "^7.53.1" (I haven't tried other versions) relieves the problem.

npearson72 avatar Apr 11 '25 14:04 npearson72

I'm having this issue as well.

I ended up removing .default for enums and booleans. However, it would be great to have default resolve/infer the type of the default value.

djshubs avatar Jun 09 '25 16:06 djshubs

I was confused for a while too, but I believe the behavior in the latest versions of the zod resolver and react-hook-form are correct. It seems like the resolver is inferring the input and output types correctly, and typing defaultValues using the input types (as I would expect). By specifying a default for a boolean field, you're saying that the form input doesn't need to specify a value for that boolean, and in one way or another your form will apply a default value so that it will always be present in the output. Here's a codesandbox demonstrating an optional boolean field: https://codesandbox.io/p/devbox/relaxed-sinoussi-cv2kd3?workspaceId=ws_Sudas5QnHp5g6vgiCpE2ic

IanVS avatar Aug 14 '25 15:08 IanVS

so what's the solution here?

abielzulio avatar Aug 31 '25 17:08 abielzulio

I'm wondering how the new zod codecs might change things

Sparticuz avatar Aug 31 '25 19:08 Sparticuz

@abielzulio I think the solution is not to specify a generic FormValues on useForm, and you probably shouldn't use .default except in rare situations (like maybe a radio button that cannot be unset, or a toggle, or a dropdown that always has a value).

IanVS avatar Sep 01 '25 14:09 IanVS

@IanVS isn't the solution just to change z.infer (aka z.output) to z.input? I think .default is fine to use, no?

And can this issue be closed now?

Garrett-R avatar Sep 01 '25 18:09 Garrett-R

Personally I'd rather let typescript infer the types itself rather than specifing overrides, but yes I think that using z.input would likely work. The full signature is useForm<TFieldValues extends FieldValues = FieldValues, TContext = any, TTransformedValues = TFieldValues>, but again, when specifying a resolver, you don't really need to provide any of those.

IanVS avatar Sep 01 '25 19:09 IanVS

im having the same issue here when doing my formschema

const formSchema = z.object({
	isFree: z.boolean().default(false),
});

const form = useForm<z.input<typeof formSchema>, any, z.infer<typeof formSchema> >({
		resolver: zodResolver(formSchema),
		defaultValues: {
			isFree: Boolean(initialData.isFree) 
		}, //initial title
	});

i think the best way is to explicitly say:

useForm<z.input<typeof formSchema>, any, z.infer<typeof formSchema>> or remove the default(false) in formSchema.

Question is is it good enough to have the defaultValues in zodResolver?

ltann avatar Oct 31 '25 22:10 ltann