jquense / yup

Dead simple Object schema validation
MIT License
22.84k stars 933 forks source link

TypeScript complains when chaining .when() calls in v1 #1912

Closed Basaingeal closed 1 year ago

Basaingeal commented 1 year ago

Describe the bug When you call when() a 2nd time in a schema, TypeScript complains with an arcane type error

To Reproduce

This is an example schema that used to work before v1.

{
    appointmentStatus: mixed<AppointmentStatus>()
      .oneOf(Object.values(AppointmentStatus))
      .required()
      .label("Appointment Status"),
    firstComeFirstServe: boolean()
      .when(["appointmentStatus"], {
        is: AppointmentStatus.Scheduled,
        then: (schema) => schema.required(),
      })
      .label("First Come First Serve"),
    timeEarly: string()
      .label("Time")
      .when(["appointmentStatus"], {
        is: AppointmentStatus.Scheduled,
        then: (schema) => schema.required(),
      })
      .when(["firstComeFirstServe"], {
        is: true,
        then: (schema) =>
          schema.label(
            "Time Early"
          ),
      }),
  };

TypeScript gives this error, concerning the 2nd when call for the timeEarly field.

This expression is not callable.    
  Each member of the union type '{ <U extends ISchema<any, any, any, any> = StringSchema<string | undefined, AnyObject, undefined, "">>(builder: ConditionBuilder<StringSchema<string | undefined, AnyObject, undefined, "">, U>): U; <U extends ISchema<...> = StringSchema<...>>(keys: string | string[], builder: ConditionBuilder<...>): U; <UThen extends...' has signatures, but none of those signatures are compatible with each other.

Expected behavior This worked fine with no type errors in v0.x

Platform (please complete the following information): TypeScript v4.9.5 yup v1.0.0

lstellway commented 1 year ago

+1

bykof commented 1 year ago

Got this bug too.

jquense commented 1 year ago

if anyone wants to take a stab at the types here be my guest, I'll get to this but a little short on time right now. It appears to be a TS only issue, the api does work as described.

bykof commented 1 year ago

@jquense i'll have a look.

bykof commented 1 year ago

so for now I am testing this combination:

object({
    type: string(),
    polyField: object()
      .when('type', {
        is: 'foo',
        then: (schema) =>
          schema.shape({
            fooField: string().required(),
          }),
      })
      .when('type', {
        is: 'bar',
        then: (schema) =>
          schema.shape({
            barField: string().required(),
          }),
      }),
  });

Error is:

TS2349: This expression is not callable.
  Each member of the union type '{ <U extends ISchema<any, any, any, any> = ObjectSchema<{}, AnyObject, {}, "">>(builder: ConditionBuilder<ObjectSchema<{}, AnyObject, {}, "">, U>): U; <U extends ISchema<...> = ObjectSchema<...>>(keys: string | string[], builder: ConditionBuilder<...>): U; <UThen extends ISchema<...> = ObjectSchema<...>, UOtherwise ...' has signatures, but none of those signatures are compatible with each other.

Need more help in this problem.

timurcatakli commented 1 year ago

+1 I reverted back to "yup": "^0.32.11",

ZahinPrangon commented 1 year ago

@jquense Not sure if this is relevant or not: I get a typescript error when I migrated from v0.32.11 to 1.0.0

The error is: Overload 1 of 4, '(keys: string | string[], builder: ConditionBuilder<StringSchema<string | undefined, AnyObject, undefined, "">, StringSchema<string | undefined, AnyObject, undefined, "">>): StringSchema<...>', gave the following error. Argument of type '{ is: (val: any) => boolean; then: StringSchema<string, AnyObject, undefined, "">; otherwise: StringSchema<Maybe<string | undefined>, AnyObject, undefined, "">; }' is not assignable to parameter of type 'ConditionBuilder<StringSchema<string | undefined, AnyObject, undefined, "">, StringSchema<string | undefined, AnyObject, undefined, "">>'. Object literal may only specify known properties, and 'is' does not exist in type 'ConditionBuilder<StringSchema<string | undefined, AnyObject, undefined, "">, StringSchema<string | undefined, AnyObject, undefined, "">>'

The code snippet is this: return myValue.showIf ? yup.string().when(myObject.key, { is: val => val === myValue.showIf, then: yup.string().required(), otherwise: yup.string().notRequired(), })

Type for showIf is string | number | undefined Type for myObject is object where it has key as key.

The error is on the is condition. Thanks!

vtrphan commented 1 year ago

@jquense Not sure if this is relevant or not: I get an typescript error when I migrated from v0.32.11 to 1.0.0

The error is: Overload 1 of 4, '(keys: string | string[], builder: ConditionBuilder<StringSchema<string | undefined, AnyObject, undefined, "">, StringSchema<string | undefined, AnyObject, undefined, "">>): StringSchema<...>', gave the following error. Argument of type '{ is: (val: any) => boolean; then: StringSchema<string, AnyObject, undefined, "">; otherwise: StringSchema<Maybe<string | undefined>, AnyObject, undefined, "">; }' is not assignable to parameter of type 'ConditionBuilder<StringSchema<string | undefined, AnyObject, undefined, "">, StringSchema<string | undefined, AnyObject, undefined, "">>'. Object literal may only specify known properties, and 'is' does not exist in type 'ConditionBuilder<StringSchema<string | undefined, AnyObject, undefined, "">, StringSchema<string | undefined, AnyObject, undefined, "">>'

The code snippet is this: return myValue.showIf ? yup.string().when(myObject.key, { is: val => val === myValue.showIf, then: yup.string().required(), otherwise: yup.string().notRequired(), })

Type for showIf is string | number | undefined Type for myObject is object where it has key as key.

The error is on the is condition. Thanks!

+1 i got this error too. Please advise

walidvb commented 1 year ago

maybe related to https://github.com/jquense/yup/issues/1899#issuecomment-1438236928 ?

jquense commented 1 year ago

Ok took a look at this, and the crux of the issue is that when now (correctly) produces a union of schema, one for each branch of the condition. TS has a hard time telling that each return value has a when method that is type compatible with the other members of the union. I will explore this a bit more but in the meantime using a a single when with the function builder covers all the same cases with a bit more code. e.g.

 object()
      .when('type', {
        is: 'foo',
        then: (schema) =>
          schema.shape({
            fooField: string().required(),
          }),
      })
      .when('type', {
        is: 'bar',
        then: (schema) =>
          schema.shape({
            barField: string().required(),
          }),
      }),

becomes:

 object().when('type', (type) => {
  if (type === 'foo') {
    return schema.shape({
      fooField: string().required(),
    });
  }
  if (type === 'bar') {
    return schema.shape({
      barField: string().required(),
    });
  }
});
jquense commented 1 year ago

I think at the moment the only thing we can do is add an overload that is less strict but produces less correct types...

eucciferri commented 1 year ago

Same issue here!

nvillabona commented 1 year ago

Got the same issue, I had to revert too

gcspindox commented 1 year ago

Got the same issue with "yup": "^1.0.2" and "typescript": "^5.0.3" the revert to yup version "yup": "^0.32.11" works fine

ChAnDuChUkKa commented 1 year ago

Got the same issue with yup version of ^1.1.1

No overload matches this call. Overload 1 of 4, '(keys: string | string[], builder: ConditionBuilder<StringSchema<string | undefined, AnyObject, undefined, "">>): StringSchema<string | undefined, AnyObject, undefined, "">', gave the following error. Argument of type '{ is: (activeType: string) => boolean; then: yup.StringSchema<string, yup.AnyObject, undefined, "">; otherwise: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">; }' is not assignable to parameter of type 'ConditionBuilder<StringSchema<string | undefined, AnyObject, undefined, "">>'. Object literal may only specify known properties, and 'is' does not exist in type 'ConditionBuilder<StringSchema<string | undefined, AnyObject, undefined, "">>'

jquense commented 1 year ago

Folks you may have a different issue, like not having updated your when conditions to match the v1 syntax. If you think you have the same issue please provide a reproduction. Saying you have the same issue, when the original issue is closed and fixed, isn't actionable for me.

hect1c commented 1 year ago

I just ran into this issue and the fix for me was to update my then to use the proper new syntax

before

yup
          .string()
          .nullable()
          .notRequired()
          .when('PHONE_IS_X_DIGITS', {
            is: (val) => val && val.length > 0,
            then: yup.string().min( // <--- issue is here; then is not correct syntax for v1
              7,
              intl.formatMessage({
                id: 'PHONE_IS_X_DIGITS',
                defaultMessage: 'Phone must be at least 7 digits',
              }),
            ),
          }),

after

yup
          .string()
          .nullable()
          .notRequired()
          .when('PHONE_IS_X_DIGITS', {
            is: (val) => val && val.length > 0,
            then: (schema) => // <---- notice here return function
              schema.min(
                7,
                intl.formatMessage({
                  id: 'PHONE_IS_X_DIGITS',
                  defaultMessage: 'Phone must be at least 7 digits',
                }),
              ),
          })

And the error went away

SSylvain1989 commented 1 year ago

for those who want a real and complex example : Here is before upgrade version :

import * as Yup from 'yup';

interface optionsType {
  id: string;
  label: string;
  value: string;
}

const formEpischema = Yup.object().shape(
  {
    attestationType: Yup.object({}).shape({
      id: Yup.string().required(),
      label: Yup.string().required(),
      value: Yup.string().required()
    }),
    activityType: Yup.object({}).when('attestationType', {
      is: (attestationType: optionsType) =>
        attestationType.label !== 'Standard',
      then: Yup.object({}).shape({
        id: Yup.string().required(),
        label: Yup.string().required(),
        value: Yup.string().required()
      }),
      otherwise: Yup.object({
        id: Yup.string(),
        label: Yup.string(),
        value: Yup.string()
      })
    }),
    shoesType: Yup.array().when(['helmetType', 'otherEquipement'], {
      is: (helmetType: optionsType[], otherEquipement: string) =>
        !helmetType?.length && !otherEquipement,
      then: Yup.array()
        .of(
          Yup.object().shape({
            id: Yup.string().required(),
            label: Yup.string().required(),
            value: Yup.string().required()
          })
        )
        .min(1)
        .required()
    }),
    shoesCriteria: Yup.object({}).shape({
      id: Yup.string(),
      label: Yup.string(),
      value: Yup.string()
    }),
    shoesSize: Yup.number().min(10).max(55),
    helmetType: Yup.array().when(['shoesType', 'otherEquipement'], {
      is: (shoesType: optionsType[], otherEquipement: string) =>
        !shoesType?.length && !otherEquipement,
      then: Yup.array()
        .of(
          Yup.object().shape({
            id: Yup.string().required(),
            label: Yup.string().required(),
            value: Yup.string().required()
          })
        )
        .min(1)
        .required()
    }),
    otherEquipement: Yup.string()
      .min(4)
      .when(['shoesType', 'helmetType'], {
        is: (shoesType: optionsType[], helmetType: optionsType[]) =>
          !shoesType?.length && !helmetType?.length,
        then: Yup.string().min(4).required()
      }),
    isCefri: Yup.boolean().oneOf([true, false]),
    dosimeterType: Yup.object({}).when('isCefri', {
      is: true,
      then: Yup.object({}).shape({
        id: Yup.string().required(),
        label: Yup.string().required(),
        value: Yup.string().required()
      }),
      otherwise: Yup.object({
        id: Yup.string(),
        label: Yup.string(),
        value: Yup.string()
      })
    }),
    dosimeterRef: Yup.string().when('isCefri', {
      is: true,
      then: Yup.string().min(7).max(7).required(),
      otherwise: Yup.string()
    })
  },
  [
    ['shoesType', 'helmetType'],
    ['shoesType', 'otherEquipement'],
    ['helmetType', 'otherEquipement']
  ]
);

export default formEpischema;

Here is now :

import * as Yup from 'yup';

interface optionsType {
  id: string;
  label: string;
  value: string;
}

const formEpischema = Yup.object().shape({
  attestationType: Yup.object({ // change is here 👈
    id: Yup.string().required(),
    label: Yup.string().required(),
    value: Yup.string().required()
  }),

  activityType: Yup.object().when('attestationType', {
    is: (attestationType: optionsType) => attestationType.label !== 'Standard',
    then: () => // change is here 👈
      Yup.object({
        id: Yup.string().required(),
        label: Yup.string().required(),
        value: Yup.string().required()
      }),
    otherwise: () => // change is here 👈
      Yup.object({
        id: Yup.string(),
        label: Yup.string(),
        value: Yup.string()
      })
  }),

  shoesType: Yup.array().when(['helmetType', 'otherEquipement'], {
    is: (helmetType: optionsType[], otherEquipement: string) => !helmetType?.length && !otherEquipement,
    then: () => // change is here 👈
      Yup.array()
        .of(
          Yup.object().shape({
            id: Yup.string().required(),
            label: Yup.string().required(),
            value: Yup.string().required()
          })
        )
        .min(1)
        .required()
  }),

  shoesCriteria: Yup.object({
    id: Yup.string(),
    label: Yup.string(),
    value: Yup.string()
  }),

  shoesSize: Yup.number().min(10).max(55),

  helmetType: Yup.array().when(['shoesType', 'otherEquipement'], {
    is: (shoesType: optionsType[], otherEquipement: string) => !shoesType?.length && !otherEquipement,
    then: () =>
      Yup.array()
        .of(
          Yup.object().shape({
            id: Yup.string().required(),
            label: Yup.string().required(),
            value: Yup.string().required()
          })
        )
        .min(1)
        .required()
  }),

  otherEquipement: Yup.string().min(4).when(['shoesType', 'helmetType'], {
    is: (shoesType: optionsType[], helmetType: optionsType[]) => !shoesType?.length && !helmetType?.length,
    then: () => Yup.string().min(4).required()
  }),

  isCefri: Yup.boolean().oneOf([true, false]),

  dosimeterType: Yup.object().when('isCefri', {
    is: true,
    then: () =>
      Yup.object({
        id: Yup.string().required(),
        label: Yup.string().required(),
        value: Yup.string().required()
      }),
    otherwise: () =>
      Yup.object({
        id: Yup.string(),
        label: Yup.string(),
        value: Yup.string()
      })
  }),

  dosimeterRef: Yup.string().when('isCefri', {
    is: true,
    then: (schema) => schema.min(7).max(7).required(),
    otherwise: () => Yup.string()
  })
    },
  [
    ['shoesType', 'helmetType'],
    ['shoesType', 'otherEquipement'],
    ['helmetType', 'otherEquipement']
  ]
);

export default formEpischema;

Old version (0.3): The is, then, and otherwise functions of conditional validations accepted objects as arguments. New version (1): The then and otherwise functions must return a modified schema. They now take empty arrow functions () => ... to return the modified schema.

Old version (0.3): Validation methods such as required(), min(), max(), etc. were called directly on the fields (for example: Yup.string().required()). New version (1): Validation methods must be called via the schema (for example: Yup.string().required() becomes Yup.string().required()).

Old version (0.3): The mixed() method was used to create a schema when the type was not specified. New version (1): The mixed() method has been removed. You must specify the type directly when creating the schema (for example: Yup.string()).

Old version (0.3): The validate() method directly returned validation errors if they existed, otherwise returned undefined. New version (1): The validate() method returns a promise. You must use await or .then() to get the results.

Old version (0.3): The isValid() method directly returned true if the validation was successful, otherwise returned false. New version (1): The isValid() method returns a promise. You must use await or .then() to get the results.

Old version (0.3): The nullable() and notRequired() methods were used to specify that a field could be null or not required. New version (1): These methods have been removed. To allow a field to be null or not required, you can simply not call any validation method after the field (eg: Yup.string().nullable() just becomes Yup.string()).

New version (1): The oneOf() method is now used to validate that a value is part of a given set. For example: Yup.string().oneOf(['value1', 'value2']). These changes reflect some of the important differences between the two versions of Yup.

gabrielab22 commented 1 year ago
const addUserValidationSchema = Yup.object().shape({
  fullName: Yup.string(),
  email: Yup.string().matches(EMAIL_REGEX, {
    message: 'Please enter valid email',
  }),
  password: Yup.string().matches(
    PASSWORD_REGEX,
    'Password must be 8 chars min, 1 uppercase, 1 lowercase, 1 digit, 1 special symbol.',
  ),
  walletAddress: Yup.string()
    .required('Address is required')
    .test('invalid-address', 'Address is invalid', (value) =>
      isValidAddress(value),
    )
    .when(['walletAddress'], {
      is: (
        value: string,
        context: { parent: { email: string; password: string } },
      ) => {},
      then: Yup.string().notRequired(),
      otherwise: Yup.string().required(),
    }),
});

Can anybody have solution for this?

LucasHayashi commented 1 year ago

@SSylvain1989 , thanks!

const registerSchema = yup.object().shape({
  data_nascimento: yup
    .string()
    .required("A data de nascimento é obrigatória")
    .length(10, "A data de nascimento está incompleta"),
  tp_sexo: yup.string().required("Selecione o gênero"),
  telefone: yup.string().optional(),
  celular: yup
    .string()
    .required("O celular é obrigatório")
    .length(15, "O celular deve ter 15 caracteres"),
  estado: yup.string().required("O estado é obrigatório"),
  cidade: yup.string().required("A cidade é obrigatória"),
  cep: yup
    .string()
    .required("O CEP é obrigatório")
    .length(9, "O CEP deve ter 9 caracteres"),
  endereco: yup.string().required("O endereço é obrigatório"),
  numero_endereco: yup
    .string()
    .required("O número da residência é obrigatório"),
  bairro: yup.string().required("O bairro é obrigatório"),
  complemento: yup.string().optional(),
  plano: yup.string().required("Selecione um plano"),
  ds_plano: yup.string().when("plano", {
    is: "8",
    then: (schema) => schema.required("Digite um plano"),
  }),
  alergia: yup.string().required("Selecione se possui alguma alergia"),
  ds_alergia: yup.string().when("alergia", {
    is: "S",
    then: (schema) => schema.required("Digite a alergia"),
  }),
  medicamento: yup.string().required("Selecione se toma algum medicamento"),
  ds_medicamento: yup.string().when("medicamento", {
    is: "S",
    then: (schema) => schema.required("Digite o medicamento"),
  }),
  cirurgia: yup.string().required("Selecione se já realizou alguma cirurgia"),
  ds_cirurgia: yup.string().when("cirurgia", {
    is: "S",
    then: (schema) => schema.required("Digite a cirurgia"),
  }),
  comorbidade: yup.string().required("Comorbidade é obrigatória"),
  ds_comorbidade: yup.string().when("comorbidade", {
    is: "21",
    then: (schema) => schema.required("Digite a comorbidade"),
  }),
});
riflan-codimite commented 1 year ago

This is my solution

interface Inputs {
  title: string;
  startTime: string;
  endTime: string;
  type: boolean;
}

const schema = yup
  .object({
    type: yup.boolean(),
    title: yup
      .string()
      .when('type', {
        is: true,
        then: (schema) => schema.required('Description must be required'),
      })
      .when('type', {
        is: false,
        then: (schema) => schema.optional(),
      }),
    startTime: yup.string().required('Start Time must be required'),
    endTime: yup.string().required('End Time must be required'),
  })
  .required();
youlchu commented 11 months ago
companyType: number()
  .required('Company Type is required')
  .typeError('Company Type must be a number'),

taxOffice: string()
  .required('Tax Office is required')
  .when('companyType', {
    is: (val: number) => val !== 2,
    then: (schema) => schema.required('Tax Office is required'),
    otherwise: (schema) => schema.notRequired(),
  }),

it works for me

ForJessicaSake commented 9 months ago

Ok took a look at this, and the crux of the issue is that when now (correctly) produces a union of schema, one for each branch of the condition. TS has a hard time telling that each return value has a when method that is type compatible with the other members of the union. I will explore this a bit more but in the meantime using a a single when with the function builder covers all the same cases with a bit more code. e.g.

 object()
      .when('type', {
        is: 'foo',
        then: (schema) =>
          schema.shape({
            fooField: string().required(),
          }),
      })
      .when('type', {
        is: 'bar',
        then: (schema) =>
          schema.shape({
            barField: string().required(),
          }),
      }),

becomes:

 object().when('type', (type) => {
  if (type === 'foo') {
    return schema.shape({
      fooField: string().required(),
    });
  }
  if (type === 'bar') {
    return schema.shape({
      barField: string().required(),
    });
  }
});

Thank you!

nhanitk14dev commented 4 months ago

I just ran into this issue and the fix for me was to update my then to use the proper new syntax

before

yup
          .string()
          .nullable()
          .notRequired()
          .when('PHONE_IS_X_DIGITS', {
            is: (val) => val && val.length > 0,
            then: yup.string().min( // <--- issue is here; then is not correct syntax for v1
              7,
              intl.formatMessage({
                id: 'PHONE_IS_X_DIGITS',
                defaultMessage: 'Phone must be at least 7 digits',
              }),
            ),
          }),

after

yup
          .string()
          .nullable()
          .notRequired()
          .when('PHONE_IS_X_DIGITS', {
            is: (val) => val && val.length > 0,
            then: (schema) => // <---- notice here return function
              schema.min(
                7,
                intl.formatMessage({
                  id: 'PHONE_IS_X_DIGITS',
                  defaultMessage: 'Phone must be at least 7 digits',
                }),
              ),
          })

And the error went away

Thanks. It's work for me