2

I'm encountering a problem while using React Hook Form with Yup for validation. I have a schema where some fields are optional, but I'm getting a type error related to the resolver.

Here's my Yup schema:

const yupSchema = yup.object({
  name: yup.string()
    .typeError('Name must be a string')
    .required('Name is required')
    .min(3, 'Name must be at least 3 characters long'),

  surname: yup.string()
    .matches(/^[a-zA-Z]+$/, 'Surname must contain only letters'),

  age: yup.number()
    .min(0, 'Age must be greater than or equal to 0')
    .max(120, 'Age must be less than or equal to 120'),
});

type YupFormData = yup.InferType<typeof yupSchema>;

In this schema, age and surname are optional fields. However, I am receiving the following error when using the resolver:

Types of parameters options and options are incompatible.
Type 'ResolverOptions<{ surname?: string | undefined; age?: number | undefined; name: string; }>' is not assignable to type 'ResolverOptions<{ name: string; surname: string | undefined; age: number | undefined; }>'.
Property 'surname' is optional in type
{
    surname?: string | undefined;
    age?: number | undefined;
    name: string;
}
but required in type
{
    name: string;
    surname: string | undefined;
    age: number | undefined;
}

Here's how I'm using the useForm hook:

const {
  control,
  handleSubmit,
  formState: {
    errors, 
  }, 
} = useForm<YupFormData>({
  resolver: yupResolver(yupSchema),
  mode: 'onBlur',
});

I would appreciate any help in resolving this issue. Thank you!

What I tried:

I created a Yup validation schema with optional fields for surname and age. I then set up the useForm hook from React Hook Form, integrating the Yup schema using the yupResolver. I expected the form to validate correctly without errors, even when the optional fields were left blank.

What I was expecting:

I expected that the optional fields (surname and age) would not cause any type errors during validation, allowing the form to submit successfully as long as the required field (name) was filled out correctly. Instead, I encountered a type error related to the resolver, indicating a mismatch in the expected types for the form data.


EDIT:

I'm also encountering an error related to the control when using the following components:

<InputController
  name="name"
  control={control}
  error={errors.name?.message as string}
  label="Name"
/>
<InputController
  name="surname"
  control={control}
  error={errors.surname?.message as string}
  label="Surname"
/>
<InputNumberController
  name="age"
  control={control}
  error={errors.age?.message as string}
  label="Age"
/>

The error message I receive is:

TS2322: Type
Control<{
    name: string;
    surname: string | undefined;
    age: number | undefined;
}, unknown, {
    surname?: string | undefined;
    age?: number | undefined;
    name: string;
}>
is not assignable to type
Control<{
    surname?: string | undefined;
    age?: number | undefined;
    name: string;
}>
The types of _options.resolver are incompatible between these types.
Type 'Resolver<{ name: string; surname: string | undefined; age: number | undefined; }, unknown, { surname?: string | undefined; age?: number | undefined; name: string; }> | undefined' is not assignable to type 'Resolver<{ surname?: string | undefined; age?: number | undefined; name: string; }, any, { surname?: string | undefined; age?: number | undefined; name: string; }> | undefined'.
Type 'Resolver<{ name: string; surname: string | undefined; age: number | undefined; }, unknown, { surname?: string | undefined; age?: number | undefined; name: string; }>' is not assignable to type 'Resolver<{ surname?: string | undefined; age?: number | undefined; name: string; }, any, { surname?: string | undefined; age?: number | undefined; name: string; }>'.
Types of parameters options and options are incompatible.
Type 'ResolverOptions<{ surname?: string | undefined; age?: number | undefined; name: string; }>' is not assignable to type 'ResolverOptions<{ name: string; surname: string | undefined; age: number | undefined; }>'.
Type '{ surname?: string | undefined; age?: number | undefined; name: string; }' is not assignable to type '{ name: string; surname: string | undefined; age: number | undefined; }'.
Property surname is optional in type
{
    surname?: string | undefined;
    age?: number | undefined;
    name: string;
}
but required in type
{
    name: string;
    surname: string | undefined;
    age: number | undefined;
}

This error with control exists even before removing <YupFormData> from the useForm hook. While removing it resolves the resolver error, the control type mismatch persists. Any guidance on how to resolve this would be greatly appreciated!


EDIT: Here is my implementation of the InputController:

type InputControllerProps<T extends FieldValues> = {
  name: Path<T>;
  control: Control<T>;
  rules?: Omit<RegisterOptions<T, Path<T>>, 'valueAsNumber' | 'valueAsDate' | 'setValueAs' | 'disabled'>;
  error?: string;
  label: string;
};

function InputController<T extends FieldValues>({
  name, control, rules, error, label,
}: InputControllerProps<T>) {
  return (
    <Controller
      name={name}
      control={control}
      rules={rules}
      render={({ field }) => (
        <input
          {...field}
          aria-label={label}
          id={name}
          style={{ borderColor: error ? 'red' : 'default' }} // Example of error handling
          placeholder={label}
        />
      )}
    />
  );
}
1
  • Basically it looks like it's telling you that "optional" is not the same as "could be undefined". Maybe it relates to this option Commented Jul 17 at 15:56

2 Answers 2

2

Thanks for the thorough question.

The root of you problem is that you are using the wrong type in the generics. Looking at the yup generic types, they are very complex and not that intuitive. I would also assume the first generic would be the shape of resulting schema but it's actually not.

I would start by removing the generic type you defined. While the generics are strange they are being inferred which can help us understand the types.

If you just call something like this...

const formTest = useForm({
  resolver: yupResolver(yupSchema),
  mode: "onBlur",
})

If you hover over it in you IDE you can see the type is...

const formTest: UseFormReturn<{
  name: string;
  surname: string | undefined;
  age: number | undefined;
}, unknown, {
  surname?: string | undefined;
  age?: number | undefined;
  name: string;
}>

These types are defined here and look like this...

export function useForm<
  TFieldValues extends FieldValues = FieldValues,
  TContext = any,
  TTransformedValues = TFieldValues,
>(
  props: UseFormProps<TFieldValues, TContext, TTransformedValues> = {},
): UseFormReturn<TFieldValues, TContext, TTransformedValues>

So you are providing the wrong type for TFieldValues. If we then look at the UseFormReturn type here, you can also see that the control is also using TFieldValues as the first generic.

export type UseFormReturn<
  TFieldValues extends FieldValues = FieldValues,
  TContext = any,
  TTransformedValues = TFieldValues,
> = {
  control: Control<TFieldValues, TContext, TTransformedValues>;
  // ...other props
}

Same goes for the resolver which is why you get the resolver error. This type should be...

Resolver<TFieldValues, TContext, TTransformedValues>

But looking at the yupResolver types we can see a better naming for these generic types.

function yupResolver<Input extends FieldValues, Context, Output>

But as far as I can tell the Input (aka TFieldValues) type is always inferred from the function arguments. You could dig through their types to see if you can find some utility type similar their yup.InferType that provides this type. Or you can just cut your losses and create your own 😅.

As best as I can tell this strange TFieldValues type is caused by yup inferring the type of TIn from MakeKeysOptional<TIn> which you can see here.

export default class ObjectSchema<
  TIn extends Maybe<AnyObject>,
  TContext = AnyObject,
  TDefault = any,
  TFlags extends Flags = '',
> extends Schema<MakeKeysOptional<TIn>, TContext, TDefault, TFlags> {

The MakeKeysOptional type is a very nested and complex type but I could see this stripping the ? on all the properties in the object schema type, I'm just not able to prove that, nor do I care to!


At the end of the day, their types are horrible! If I were you I would do this...

type ExtractInput<T> = T extends yup.ObjectSchema<infer Input, any, any, any>
  ? Input
  : never;

export type Input = ExtractInput<typeof yupSchema>;
export type Context = unknown; // idk what this is but it's unknown for your example
export type Output = yup.InferType<typeof yupSchema>;

const {
  control,
  handleSubmit,
  formState: {
    errors, 
  }, 
} = useForm<Input, Context, Output>({
  resolver: yupResolver(yupSchema),
  mode: "onBlur",
});

Note: Do not type to pass generic type arguments to yupResolver it also has different issues, but it falls inline perfectly when the types are inferred from the yupSchema you pass it.

Then the last thing is to update your `` component types to also align with the Input, Context and Output types.

type InputControllerProps<
  In extends FieldValues = FieldValues,
  Ctx = any,
  Out = In
> = {
  name: Path<In>;
  control: Control<In, Ctx, Out>;
  rules?: Omit<
    RegisterOptions<In, Path<In>>,
    "valueAsNumber" | "valueAsDate" | "setValueAs" | "disabled"
  >;
  error?: string;
  label: string;
};

export function InputController<
  In extends FieldValues = FieldValues,
  Ctx = any,
  Out = In
>({ name, control, rules, error, label }: InputControllerProps<In, Ctx, Out>) {
  return (
    <Controller
      name={name}
      control={control}
      rules={rules}
      render={({ field }) => (
        <input
          {...field}
          aria-label={label}
          id={name}
          style={{ borderColor: error ? "red" : "default" }} // Example of error handling
          placeholder={label}
        />
      )}
    />
  );
}

Now all your types are aligned with no errors 🎉!

Sign up to request clarification or add additional context in comments.

Comments

0

You can try adding .optional() to the schema for the optional values.

surname: yup.string()
    .optional()
    .matches(/^[a-zA-Z]+$/, 'Surname must contain only letters'),

age: yup.number()
    .optional()
    .min(0, 'Age must be greater than or equal to 0')
    .max(120, 'Age must be less than or equal to 120'),

2 Comments

Thanks, I tried that, it doesn't change anything.
Unfortunately adding .optional() does not change the resulting schema type as all object properties are assumed to be optional unless explicitly .required() or otherwise .default()'d.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.