forge42dev / remix-hook-form

Open source wrapper for react-hook-form aimed at Remix.run
MIT License
330 stars 27 forks source link

TypeScript: Return type of useRemixForm hook is incorrect. #75

Closed piotrkulpinski closed 2 months ago

piotrkulpinski commented 7 months ago

Hi, Thanks for building this package! I'm trying to migrate my app from remix-validated-form to this one but I've run into some issue regarding TS types.

Loos like useRemixForm is incorrectly returning form types.

Here's the type returned from react-hook-form: CleanShot 2024-02-07 at 15 05 53

...and here's remix-hook-form: CleanShot 2024-02-07 at 15 05 44

You can see the values are incorrectly typed as undefined. According to the zod schema, those fields should never be undefined just like in the first example.

const schema = z.object({
  name: z.string().trim().min(3).max(32),
  slug: z.string().trim().min(3).max(32),
})
AlemTuzlak commented 7 months ago

hi @piotrkulpinski, I'll look into this but you can pass in the schema type as a workaround and it will work correctly, eg. useRemixForm<YourSchema>()

piotrkulpinski commented 7 months ago

hi @piotrkulpinski, I'll look into this but you can pass in the schema type as a workaround and it will work correctly, eg. useRemixForm<YourSchema>()

Tried that, but this has given me even bigger problems with typing the models correctly.

CleanShot 2024-02-12 at 13 23 43

chiptus commented 6 months ago

same happens when I want to pass register to a component. using UseFormRegister<FormData> doesn't work:

Type '(name: "method" | "email" | "phone", options?: (RegisterOptions<{ method: "whatsapp" | "email"; email: string; phone: string; }> & { disableProgressiveEnhancement?: boolean | undefined; }) | undefined) => { ...; }' is not assignable to type 'UseFormRegister<{ method: "whatsapp" | "email"; email: string; phone: string; }>'.
  Types of parameters 'options' and 'options' are incompatible.
    Type 'RegisterOptions<{ method: "whatsapp" | "email"; email: string; phone: string; }, TFieldName> | undefined' is not assignable to type '(RegisterOptions<{ method: "whatsapp" | "email"; email: string; phone: string; }> & { disableProgressiveEnhancement?: boolean | undefined; }) | undefined'.
      Type 'Partial<{ required: string | ValidationRule<boolean>; min: ValidationRule<string | number>; max: ValidationRule<string | number>; ... 9 more ...; deps: string | string[]; }> & { ...; }' is not assignable to type '(RegisterOptions<{ method: "whatsapp" | "email"; email: string; phone: string; }> & { disableProgressiveEnhancement?: boolean | undefined; }) | undefined'.
        Type 'Partial<{ required: string | ValidationRule<boolean>; min: ValidationRule<string | number>; max: ValidationRule<string | number>; ... 9 more ...; deps: string | string[]; }> & { ...; }' is not assignable to type 'Partial<{ required: string | ValidationRule<boolean>; min: ValidationRule<string | number>; max: ValidationRule<string | number>; ... 9 more ...; deps: string | string[]; }> & { ...; } & { ...; }'.
          Type 'Partial<{ required: string | ValidationRule<boolean>; min: ValidationRule<string | number>; max: ValidationRule<string | number>; ... 9 more ...; deps: string | string[]; }> & { ...; }' is not assignable to type 'Partial<{ required: string | ValidationRule<boolean>; min: ValidationRule<string | number>; max: ValidationRule<string | number>; ... 9 more ...; deps: string | string[]; }>'.
            Types of property 'validate' are incompatible.
              Type 'Validate<TFieldName extends `${infer K}.${infer R}` ? K extends "method" | "email" | "phone" ? R extends Path<{ method: "whatsapp" | "email"; email: string; phone: string; }[K]> ? PathValue<...> : never : K extends `${number}` ? never : never : TFieldName extends "method" | ... 1 more ... | "phone" ? { ...; }[TField...' is not assignable to type 'Validate<string, { method: "whatsapp" | "email"; email: string; phone: string; }> | Record<string, Validate<string, { method: "whatsapp" | "email"; email: string; phone: string; }>> | undefined'.
                Type 'Validate<TFieldName extends `${infer K}.${infer R}` ? K extends "method" | "email" | "phone" ? R extends Path<{ method: "whatsapp" | "email"; email: string; phone: string; }[K]> ? PathValue<...> : never : K extends `${number}` ? never : never : TFieldName extends "method" | ... 1 more ... | "phone" ? { ...; }[TField...' is not assignable to type 'Validate<string, { method: "whatsapp" | "email"; email: string; phone: string; }> | Record<string, Validate<string, { method: "whatsapp" | "email"; email: string; phone: string; }>> | undefined'.
                  Type 'Validate<TFieldName extends `${infer K}.${infer R}` ? K extends "method" | "email" | "phone" ? R extends Path<{ method: "whatsapp" | "email"; email: string; phone: string; }[K]> ? PathValue<...> : never : K extends `${number}` ? never : never : TFieldName extends "method" | ... 1 more ... | "phone" ? { ...; }[TField...' is not assignable to type 'Validate<string, { method: "whatsapp" | "email"; email: string; phone: string; }>'.
                    Type 'string' is not assignable to type 'TFieldName extends `${infer K}.${infer R}` ? K extends "method" | "email" | "phone" ? R extends Path<{ method: "whatsapp" | "email"; email: string; phone: string; }[K]> ? PathValue<...> : never : K extends `${number}` ? never : never : TFieldName extends "method" | ... 1 more ... | "phone" ? { ...; }[TFieldName] : T...'.ts(2322)
SignInMethodSelector.tsx(20, 3): The expected type 
AlemTuzlak commented 6 months ago

Hmm can you guys install the latest versions of remix-hook-form and react-hook-form and let me know if that resolves it? It could be a mismatch between the versions, but what is important to note is that the return types ARE different, there are a few changes added on top of react-hook-form so they can't be identical, like handleSubmit, register, reset etc are enhanced for remix use-cases

usr3 commented 6 months ago

Hi @AlemTuzlak, any way you can expose the equivalent type of UseFormReturn which remix-hook-form is using? I'm passing the form object to a custom hook so no way to init the form there.

export function useSomeCustomHook(
  setSomeState: React.Dispatch<React.SetStateAction<boolean>>,
  form: UseFormReturn,
) {...}

I'm using the latest versions of react-hook-form & remix-hook-form.

makrandgupta commented 6 months ago

Running into the same issue. I have the latest versions of both react-hook-form and remix-hoko-form.

Here's the error I am seeing:

Type '{ children: Element; handleSubmit: (e?: BaseSyntheticEvent<object, any, any> | undefined) => Promise<void>; reset: (values?: { email: string; password: string; } | { ...; } | undefined, options?: Partial<...> | undefined) => void; ... 12 more ...; setFocus: UseFormSetFocus<...>; }' is not assignable to type 'UseFormReturn<{ email: string; password: string; }, any, undefined>'.
  Types of property 'reset' are incompatible.
    Type '(values?: { email: string; password: string; } | { email?: string | undefined; password?: string | undefined; } | undefined, options?: Partial<{ keepDirtyValues: boolean; keepErrors: boolean; keepDirty: boolean; ... 7 more ...; keepSubmitCount: boolean; }> | undefined) => void' is not assignable to type 'UseFormReset<{ email: string; password: string; }>'.
      Types of parameters 'values' and 'values' are incompatible.
        Type '{ email: string; password: string; } | { email?: string | undefined; password?: string | undefined; } | ResetAction<{ email: string; password: string; }> | undefined' is not assignable to type '{ email: string; password: string; } | { email?: string | undefined; password?: string | undefined; } | undefined'.
          Type 'ResetAction<{ email: string; password: string; }>' is not assignable to type '{ email: string; password: string; } | { email?: string | undefined; password?: string | undefined; } | undefined'.ts(2322)

My code:


const schema = z.object({
  email: z
    .string({ required_error: "Email is required" })
    .email("Email is invalid"),
  password: z.string({ required_error: "Password is required" }),
});
type FormData = z.infer<typeof schema>;

// Form component is from shadcn
export default function Login() {
  const form = useRemixForm<FormData>({
    mode: "onSubmit",
    resolver,
  });
  return (
      <FormProvider {...form}>
        <form onSubmit={form.handleSubmit} className="space-y-8">
          <FormField
            control={form.control}
            name="email"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Email</FormLabel>
                <FormControl>
                  <Input placeholder="shadcn" {...field} />
                </FormControl>
                <FormDescription>
                  This is your public display name.
                </FormDescription>
                <FormMessage />
              </FormItem>
            )}
          />
          <FormField
            control={form.control}
            name="password"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Password</FormLabel>
                <FormControl>
                  <Input placeholder="shadcn" {...field} />
                </FormControl>
                <FormDescription>
                  This is your public display name.
                </FormDescription>
                <FormMessage />
              </FormItem>
            )}
          />
          <Button type="submit">Submit</Button>
        </form>
      </FormProvider>
  );
}

Update: fixing the above error by manually setting reset, gives another error:

Conversion of type '(e?: BaseSyntheticEvent<object, any, any> | undefined) => Promise<void>' to type 'UseFormHandleSubmit<{ email: string; password: string; }, { email: string; password: string; }>' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
  Types of parameters 'e' and 'onValid' are incompatible.
    Type 'SubmitHandler<{ email: string; password: string; }>' is not comparable to type 'BaseSyntheticEvent<object, any, any>'.ts(2352)
AlemTuzlak commented 2 months ago

@makrandgupta It seems you're using the FormProvider from react-hook-form the two are not interchangeable as the remix-hook-form package augments certain functions to allow for easier handling in Remix. @usr3 I now export UseRemixFormReturn type

AlemTuzlak commented 2 months ago

a lot of issues arise from the fact that remix-hook-form has different types compared to react-hook-form, I'm closing this for now as this was opened a while ago and I've changed the signatures of many of these functions, please feel free to open new issues!