Search code examples
reactjstypescriptyup

Yup - Typing a retrieved field schema from an Object schema


I've finally decided to upgrade from Yup 0.29 to 1.2 and faced some issues with it's types. I'm Looking for help to find a best solution for typing yup schemas.

In 0.29 quite universal type Schema used to fit everywhere, but now it doesn't.

1. Yup Object Generic.

Here I Expect for compiler to complain, because Yup Object Schema doesn't satisfy my Generic type, how do I make this schema to be invalid because of MyType? Also to be more strict, it should complain when field is not optional, but .required() or .nullable() is not set.

interface MyType {
  id: number;
  name: string;
}

const mySchema = yup.object<MyType>({
  id: yup.number().required(),
  // name: yup.string().required(), <--- Here compiler used to raise an error in 0.29
}).required()

Edit: First issue is solved, found an explanation here

2. Typing a retrieved field schema from an Object schema

Here I have components and functions, where I have an argument - a field schema (string/number/object, etc)

interface MyType {
  id: number;
  name: string;
}

const mySchema = yup.object<MyType>({
  id: yup.number().required(),
  name: yup.string().required(),
}).required()

const idSchema = yup.reach(mySchema, "id");
const nameSchema = yup.reach(mySchema, "name");

function validateField(fieldSchema: Schema) {
  fieldSchema.validate("TEST")
}

validateField(idSchema) // <--- Here compiler complains, where in 0.29 it didn't

field schema error

I'm not quite sure about this Reference | ISchema type, cannot find Reference type in Yup exported interfaces.


Solution

  • Per this PR, which was made almost a year ago, returning a type of Reference and ISchema is supposed to make the types more intelligent. You'll see that reach internally makes use of getIn, which goes through the schema to return either an ISchema or a Reference, this is better because when one defines a schema, they might have a Reference field using ref -

    import { ref, object, string } from 'yup';
    
    let schema = object({
      baz: ref('foo.bar'),
      foo: object({
        bar: string(),
      }),
      x: ref('$x'),
    });
    
    schema.cast({ foo: { bar: 'boom' } }, { context: { x: 5 } });
    // => { baz: 'boom',  x: 5, foo: { bar: 'boom' } }
    

    And the ref creates an object of type (you guessed it), Reference. Note that the other types such as object, number etc., all extends Schema (parsed recursively), but that's not the case for a reference created by ref, so, when you're trying to reach and you have a schema like the one showed above, you might get a Reference or Schema, and hence the added types are better.

    How to get around for your use case?

    You can make use of an assertion utility isSchema, provided by the library, like so -

    import yup, { isSchema, ISchema, ref } from "yup";
    
    type Reference<TValue = unknown> = ReturnType<(typeof ref<TValue>)>;
    
    interface MyType {
        id: number;
        name: string;
    }
    
    const mySchema = yup.object<MyType>({
        id: yup.number().required(),
        name: yup.string().required(),
    }).required();
    
    const idSchema = yup.reach(mySchema, "id");
    const nameSchema = yup.reach(mySchema, "name");
    
    function validateField<T>(field: ISchema<T> | Reference) {
        if (isSchema(field)) return field.validate("TEST");
    
        throw new Error("Can't validate a Reference without a schema");
        // Or add your own implementation for validating reference
        // you can modify signature of validateField for that
    }
    
    validateField(idSchema); // no error
    validateField(nameSchema); // no error
    

    Here's a Playground link.