jquense / yup

Dead simple Object schema validation
MIT License
22.72k stars 925 forks source link

InferType ignores usage of `.when` in version `1.0.1` and later #2197

Open Svish opened 5 months ago

Svish commented 5 months ago

Describe the bug

Our schemas are often based on { label: string; value: T } shaped objects, and I've been using when on the object to adjust the validation of the value when its validation depends on other values. This is (as far as I've found) the only way to do this, since value is a child and cannot "see up" and target the values of other sibling objects.

We're also using react-hook-form which has type-checking of field names. These are based on the shape of of the inferred type, so it's important that the inferred shape of the schema is actually correct. E.g. for the field name foo.bar to be type-checked correctly, the inferred shape of the schema needs to contain e.g. { foo: { bar: number } }.

This was all working great in version 1.0.0, but when I tried to upgrade to latest version this week, I found that it's broken now, and seems like it was broken already in version 1.0.1. This prevents us from upgrading, as I'm unable to find a workaround to the issue that allows me to write the schema properly while also getting the expected inferred type structure.

To Reproduce

I've tried to create a sandbox, but not familiar with it and can't figure out how to make a typescript version of it, but reproduction should be very easy. Just paste the following into a typescript-file with yup version 1.0.1 or later:

import { object, string, number, boolean, type InferType } from 'yup';

const s = object({
  number: object({
    label: string().required(),
    value: number().required().min(0),
  }),
  choice: object({
    label: string().required(),
    value: boolean().required(),
  }),
  dependent: object({
    label: string().required(),
  }).when(
    ['choice.value', 'number.value'],
    ([choiceValue, numberValue], schema) => {
      return typeof numberValue === 'number' && choiceValue === true
        ? schema.shape({
            // `value` should be a required number with `numberValue` as max value when `choiceValue` is `true`
            value: number().required().min(0).max(numberValue),
          })
        : schema.shape({
            // `value` should always be `null` when `choiceValue` is `false`
            value: y.number().nullable().transform(() => null),
          });
    }
  ),
});

type V = InferType<typeof s>['dependent']['value'];
//                                        ^^^^^^^
// Property 'value' does not exist on type '{ label: string; }'

The InferType seem to completely ignore the types coming from when, while in version 1.0.0 it would actually come through properly. The { is: true, then: ..., otherwise: ... } variant of .when also have the same issue. This means the type of dependent becomes just { label: string }, instead of { label: string; value: number | undefined | null } which it was in version 1.0.0.

Expected behavior

With yup version 1.0.0 the types coming from .when are picked up properly, and the type of V becomes number | undefined | null, as expected, with no error.

Additional context

We're on Typescript version 5.0.4 right now, but I've tried to upgrade to the latest (5.4.4) just to see if this particular bug would disappear then, but no, seems to only be an issue with yup.

Svish commented 5 months ago

I'm guessing the bug was introduced in 60fe3b06f2dde6d1663d444e8eacafc61909b16e, where the types of TThen and TOtherwise seem to have been remove for whatever reason. This change makes it impossible to adjust the members of an object using when, which makes it impossible to adjust validation of object members based on siblings. Or at least I'm unable to see how to do this now. 😕

zumm commented 2 months ago

I can confirm this isn't working. But as workaround you can predefine desired type and then override it with when:

const SomeSchema = object({
  condition: boolean().required(),
  dependant: number().nullable().when('condition', {
    is: true,
    then: schema => schema.nonNullable().required(),
    otherwise: schema => schema.tansform(() => null),
  })
})

@jquense Could you enlighten us please if it's bug or intended behavior?