Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

issue: can't get typescript to accept different schema output using zod #9600

Closed
1 task done
diego3g opened this issue Dec 16, 2022 · 11 comments · Fixed by #9735
Closed
1 task done

issue: can't get typescript to accept different schema output using zod #9600

diego3g opened this issue Dec 16, 2022 · 11 comments · Fixed by #9735
Labels
enhancement New feature or request status: working in progress working in progress TS Typescript related issues

Comments

@diego3g
Copy link

diego3g commented Dec 16, 2022

Version Number

7.38.0

Codesandbox/Expo snack

https://codesandbox.io/s/hidden-cloud-vruf30?file=/src/App.tsx:0-718

Steps to reproduce

  1. Open the Code Sandbox (https://codesandbox.io/s/hidden-cloud-vruf30?file=/src/App.tsx:0-718);
  2. Look at the error at line 25;

Expected behaviour

React Hook Form should automatically infer the output type for the schema sent via the resolver property or allow us to set the input and output schema types when using useForm hook.

What browsers are you seeing the problem on?

Chrome

Relevant log output

No response

Code of Conduct

  • I agree to follow this project's Code of Conduct
@diego3g
Copy link
Author

diego3g commented Dec 16, 2022

If I change z.input for z.infer and pass it to useForm, I get the wrong typings for defaultValues:

const formSchema = z.object({
  myInput: z.string().transform(Number)
});

type FormSchema = z.infer<typeof formSchema>;

export default function App() {
  const { register, handleSubmit } = useForm<FormSchema>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      myInput: 'Hello World' // TS error: myInput must be a number!
    }
  });

  function handleFormSubmit(data: FormSchemaOutput) {}

  return (
    <form onSubmit={handleSubmit(handleFormSubmit)}>
      <input type="text" {...register("myInput")} />
    </form>
  );
}

@diego3g diego3g closed this as completed Dec 16, 2022
@diego3g diego3g reopened this Dec 16, 2022
@bluebill1049
Copy link
Member

hi, @diego3g thanks for the issue report. I don't think this is possible to support different schema outputs with the current design without breaking the existing type inference. However, I am open to suggestions or potential solutions.

The following example.

const formSchema = z.object({
  myInput: z.string().transform(Number)
});

The transformation occurs runtime inside the Zod resolver and hence returned number type, so the type is determined and inherited from that.

Here is a current workaround for your use case

export default function App() {
  const { register, handleSubmit } = useForm<FormSchemaOutput>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      myInput: 0 // use number as defaultValue
    }
  });

  return (
    <form onSubmit={handleSubmit(data => data)}>
      <input type="text" {...register("myInput", {valueAsNumber: true})} /> // setValueAsNumber to transform within hook form
    </form>
  );
}

@bluebill1049 bluebill1049 added the question Further information is requested label Dec 16, 2022
@diego3g
Copy link
Author

diego3g commented Dec 19, 2022

Hello @bluebill1049, this was just an example, my schema is bigger than that and I can't use the default values using the output schema as these values are used to fill a field array with a lot of options.

I think React Hook Form should somehow receive the z.infer type output inside the useForm generic and extract the input and output from that when using zodResolver.

I can work on a PR with that, just let me know if this is something you think is cool.

@bluebill1049
Copy link
Member

I can work on a PR with that, just let me know if this is something you think is cool.

Yea would love to see a PR on this. Thank you in advance.

@bluebill1049 bluebill1049 added enhancement New feature or request and removed question Further information is requested labels Dec 19, 2022
@kenfdev
Copy link

kenfdev commented Dec 21, 2022

First of all, thanks for this awesome library!

I'm also hitting this problem recently and found this issue.

Here's a Code Sandbox to show a slightly complex type.

https://codesandbox.io/s/react-hook-form-zod-type-error-v9w19n

I haven't dug into the RHF's code base much, but as @diego3g mentioned, I would love to pass another generic type to the useForm function.

Something like this:

useForm<InputSchema, OutputSchema>({
    resolver: zodResolver(
      inputSchema.transform((v) => ({
        categories: v.categories.value
      }))
    ),
    defaultValues: {
      categories: { value: "category3" }
    }
  });

@Brendonovich
Copy link

Figured I'd pop in and share a similar use case with File and FileList.
Say you have a form that has an image field and you want to validate certain attributes of it with zod, one way is to do a z.custom with FileList, validate that, transform it to a File, validate that, and then return it to RHF:

const validationSchema = z.object({
  image: z
    .custom<FileList>()
    .refine((file) => file?.length == 1, 'Image is required.')
    .transform(([file]) => file)
    .refine((file) => file?.size <= MAX_FILE_SIZE, `Max image size is 10MB.`)
    .refine(
      (file) => ACCEPTED_IMAGE_TYPES.includes(file?.type),
      'Only .jpg, .jpeg, .png and .webp formats are supported.'
    ),
});

Now lets say you want to preview the image that has been selected before the form is submitted, watch is a great way to do that. The only problem is that while watch('image') will give File | undefined, the zod transformation has not taken place yet, so the actual value is FileList | undefined, requiring a type override:

export function App() {
  const form = useForm<ValidationSchema>({
    resolver: zodResolver(validationSchema),
  });

  const imageList = form.watch('image', undefined) as unknown as
    | FileList
    | undefined;

  const imageUrl = useMemo(() => {
    const image = imageList?.item(0);

    if (image) {
      return URL.createObjectURL(image);
    }
  }, [imageList]);

  return (
    <form onSubmit={form.handleSubmit(console.log)}>
      <div>
        <input type="file" {...form.register('image')} />
        {imageUrl ? <img src={imageUrl} /> : <ImagePlaceholder />}
      </div>
    </form>
  );
}

Thankfully z.input exists (to my surprise!) and it should be possible to add the concept of an input schema to RHF, hopefully without breaking existing typings. I'd imagine that input schemas would need to be passed as a third generic to avoid a breaking change being required, but even if a breaking change was made it may not be wise to place the input schema generic before the output - Imo in most cases the output schema will be accurate, only in more complex cases will an input schema be needed, and then where to put TContext... idk but hopefully a solution can be implemented!

@bluebill1049
Copy link
Member

Thanks, @Brendonovich for the feedback. I think the new generic we are proposing are transformed result at the runtime, by most of the use cases a single FormValues type should represent the form values. To avoid breaking change, we may end up something such below:

useForm<FieldValues, Context, TransformedFieldValues>();

@bluebill1049
Copy link
Member

Any thoughts on the following API? it can go alone well with the new "potential" Form component

const defaultValues = { test: 'test' };

type FormValues = typeof defaultValues;
type TransformedFormValue = {
  test: number;
};

const Test = () => {
  const { control } = useForm({
    defaultValues: { test: 'test' },
  });

  return (
    <Form<FormValues, TransformedFormValue>
      onSubmit={(data) => {
        console.log(data); // data.test -> number
      }}
      control={control} // optional if not using context
    >
      <input />
    </Form>
  );
};

@bluebill1049 bluebill1049 added status: under investigation aware of this issue and pending for investigation TS Typescript related issues labels Jan 7, 2023
@pieter-berkel

This comment was marked as off-topic.

@iambryanhaney
Copy link

iambryanhaney commented Feb 10, 2023

@bluebill1049 That API looks good and would be much appreciated. I would really like to see useForm infer the input and output types (z.input<typeof schema> and z.output<typeof schema> in the case of Zod) via the resolver.

const testSchema = z.object({
  test: z.string().transform(Number),
});

const { handleSubmit } = useForm({
  defaultValues: { }, 
  // ^? { test?: string | undefined } | undefined
  resolver: zodResolver(testSchema),
});

<form
  onSubmit={handleSubmit((payload) => {
     //                    ^? { test: number }
  }}
>

Currently implementing this like so:

const testShema = z.object({
  test: z.string().transform(Number),
});

type TestFormFields = z.input<typeof testSchema>;
type TestFormPayload = z.output<typeof testSchema>;

const { handleSubmit } = useForm<TestFormFields>({
  defaultValues: { },
  // ^? { test?: string | undefined } | undefined
  resolver: zodResolver(testSchema),
});

<form
  onSubmit={handleSubmit((values: unknown) => {
    const data = values as TestFormPayload;
      //   ^?  { test: number }
  }}
>

@abhinav2712

This comment was marked as spam.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jun 5, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement New feature or request status: working in progress working in progress TS Typescript related issues
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants