Skip to content

Instantly share code, notes, and snippets.

@titouancreach
Created July 22, 2024 14:21
Show Gist options
  • Save titouancreach/8fe9bd9b3e89b1aafd963b30086a571c to your computer and use it in GitHub Desktop.
Save titouancreach/8fe9bd9b3e89b1aafd963b30086a571c to your computer and use it in GitHub Desktop.

hook-form / effect-ts resolver thread

Hello, When we use a schema with a non-symmetrical transformation like this one

const Schema = S.Struct({
  createdAt: S.DateFromString
})

We have a problem because react-hook-form and the resolver infer this schema as

{ readonly createdAt: Date }
const form = useForm({
  resolver: effectTsResolver(Schema),
  defaultValues: {
    createAt: "",
//             ^ Type 'string' is not assignable to type 'Date'.ts(2322)
  },
});

And this is more problematic when we try to use this field

  <FormField
	control={form.control}
	name="createAt"
	render={({ field }) => {
	  return (
		<FormItem>
		  <FormControl>
			<Input {...field} />
			{*/ ^ Type '{ onChange: (...event: any[]) => void; onBlur: Noop; value: Date; disabled?: boolean | undefined; name: "createAt"; ref: RefCallBack; }' is not assignable to type 'InputProps'. /*}
		  </FormControl>
		</FormItem>
	  );
	}}
  />

Yes because, at this moment, the input value type should be a string not a Date.

So the current implementation requires us to only use symmetrical schema with filters:

like:

const Schema = S.Struct{{
  createdAt: S.String.pipe(
    MyStringIsValidDateFilter()
  )
}}

and then, in the handleSubmit handler:

onSubmit={handleSubmit(data => {
//.                     ^ { created: string}
	const date = new Date(data);
})}

this looks redundant because we already have a way to express we want a Date from a string.

So i've looked at the source, we have a notion of TTransformedValue. It's not very documented but it looks that this is the type after they are transformed by the validator.

So I switched the types of the resolver to match this:

export type Resolver = <A extends FieldValues, I extends FieldValues, TContext>(
  schema: Schema.Schema<A, I>,
  config?: ParseOptions,
) => (
  values: I,
  _context: TContext | undefined,
  options: ResolverOptions<I>,
) => Promise<ResolverResult<I>>;

instead of

export type Resolver = <A extends FieldValues, I, TContext>(
  schema: Schema.Schema<A, I>,
  config?: ParseOptions,
) => (
  values: A,
  _context: TContext | undefined,
  options: ResolverOptions<A>,
) => Promise<ResolverResult<A>>;

I have the good types :

type To = S.Schema.Type<typeof Schema>;
type From = S.Schema.Encoded<typeof Schema>;

export function MyForm() {
  const form = useForm<From, any, To>({ // to type correctly here
    resolver: effectTsResolver(Schema),

    defaultValues: {
      createAt: "",
            //   ^ yeahh 
    },
  });
  
  return (
    <div>
      <Form {...form}>
        <form onSubmit={form.handleSubmit((data) => console.log(data))}>
                                        //  ^ yeah it's a Date !!!!  
          <FormField
            control={form.control}
            name="createAt"
            render={({ field }) => {
              return (
                <FormItem>
                  <FormControl>
                    <Input {...field} type="datetime-local" />
                              // works here too
                  </FormControl>
                </FormItem>
              );
            }}
          />
	     </form>
       </Form>
     </div>
  );
}

This is an example for Date but it's very powerful when we encode missing or bad information into Option for example. Like with: OptionFromUndefinedOr.

Soo, if you are agree with that change, I can write a PR and change the example in the readme of react-hook form.

Curious to have your feedbacks !!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment