yup: Missing generics in create() methods

For example, see string.ts: https://github.com/jquense/yup/blob/3ca0ebf2c26716e089316e6938deecb291c7c5e7/src/string.ts#L26-L34

The generated string.d.ts ends up with a signature of, which makes the generic impossible to constrain: export declare function create(): StringSchema<string | undefined, Record<string, any>, string | undefined>;

The solution is to make the create() function generic, e.g.:

export function create< 
   TType extends Maybe<string> = string | undefined, 
   TContext extends AnyObject = AnyObject, 
   TOut extends TType = TType 
 >() { 
   return new StringSchema<TType, TContext, TOut>(); 
}

Based on quickly skimming your codebase (I may have missed some), it looks like this applies to the following:

I’m trying to upgrade from yup@0.27.0 + @types/yup@0.26.22 to just yup@0.32.6 (without the DefinitelyTypes @types definitions), but I can’t because your type definitions would require too much unsafe casting. If you don’t have time to fix this yourself, let me know and I’ll make a PR.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 3
  • Comments: 16 (15 by maintainers)

Most upvoted comments

sure, if you want to ensure a schema matches an object you still can, but the signature is a bit different. Now you would do something like:

interface LoginFormValues {
    readonly user: string;
    readonly password: string;
}

export const loginValidatorValid: yup.SchemaOf<LoginFormValues> = yup.object({
    user: yup.string().required(),
    password: yup.string().required()
});

Hmm, I can’t reproduce that… this works for me:

export function create<
    TType extends Maybe<string> = string | undefined,
    TContext extends AnyObject = AnyObject,
    TOut extends TType = TType
    >() {
  return new StringSchema<TType, TContext, TOut>();
}

With a new test file:

import { object, string, StringSchema } from '../src';
import { AnyObject, OptionalObjectSchema } from '../src/object';

it("should compile", () => {
   // I made the type explicit here to force a compile error if it didn't match
    const foo: OptionalObjectSchema<{ a: StringSchema<string | undefined, AnyObject>}> = object({ a: string() });
})

By the way, I don’t think you’re type checking your test files. I had to add @types/jest as a dev dependency, remove a line with a type error from test/types.ts, and then finally run tsc --build tsconfig.json from tests directory. On the bright side, my test snippet above compiles.


Have you tried comparing your code against the DefinitelyTyped definitions? I know they’re a little bit out of date (v0.29 according to the comment at the top of file), but they handle generics properly. Another issue I noticed is your ObjectShape definition which uses Record: https://github.com/jquense/yup/blob/3ca0ebf2c26716e089316e6938deecb291c7c5e7/src/object.ts#L30-L33

Whereas DefinitelyTyped uses [field in keyof T] to constrain the keys: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/e539b14e9b5c4359ffaa67edbb845998d4fe8e27/types/yup/index.d.ts#L354-L360

The advantage of DefinitelyTyped’s version is that it allows stricter value typing like this:

interface LoginFormValues {
    readonly user: string;
    readonly password: string;
}
const loginValidator = yup.object<LoginFormValues>().shape({
    user: yup.string().required(),
    password: yup.string().required()
});

This is a contrived example, but I use this a ton with a large codebase at work where I’m passing form values around between yup, Formik, and Redux. If I changed LoginFormValues to be a Record I would lose type safety, which is bad.

Sure, here’s a few examples of how DefinitelyTyped’s version provides type safety:

import * as yup from "yup";

interface LoginFormValues {
    readonly user: string;
    readonly password: string;
}

// Compiles:
export const loginValidatorValid = yup.object<LoginFormValues>({
    user: yup.string().required(),
    password: yup.string().required()
});
// ERROR:
// Argument of type '{ user: yup.StringSchema<string>; passworx: yup.StringSchema<string>; }' is not assignable to parameter of type 'ObjectSchemaDefinition<LoginFormValues>'.
//  Object literal may only specify known properties, but 'passworx' does not exist in type 'ObjectSchemaDefinition<LoginFormValues>'. Did you mean to write 'password'?
export const loginValidatorTypo = yup.object<LoginFormValues>({
    user: yup.string().required(),
    passworx: yup.string().required()
});
// ERROR:
// Argument of type '{ user: yup.StringSchema<string>; password: yup.StringSchema<string>; shouldNotBeHere: yup.StringSchema<string>; }' is not assignable to parameter of type 'ObjectSchemaDefinition<LoginFormValues>'.
//  Object literal may only specify known properties, and 'shouldNotBeHere' does not exist in type 'ObjectSchemaDefinition<LoginFormValues>'.
export const loginValidatorExtraField = yup.object<LoginFormValues>({
    user: yup.string().required(),
    password: yup.string().required(),
    shouldNotBeHere: yup.string().required()
});
// ERROR:
// Type 'NumberSchema<number>' is not assignable to type 'Schema<string> | Ref'.
//  Type 'NumberSchema<number>' is not assignable to type 'Schema<string>'.
//    Types of property 'concat' are incompatible.
//      Type '(schema: NumberSchema<number>) => NumberSchema<number>' is not assignable to type '(schema: Schema<string>) => Schema<string>'.
//        Types of parameters 'schema' and 'schema' are incompatible.
//          Type 'Schema<string>' is missing the following properties from type 'NumberSchema<number>': min, max, lessThan, moreThan, and 8 more.
export const loginValidatorWrongValueType = yup.object<LoginFormValues>({
    user: yup.string().required(),
    password: yup.number().required()
});

Whereas using your types from yup@0.32.6 (import and interface omitted):

// ERROR:
// Type 'LoginFormValues' does not satisfy the constraint 'Record<string, AnySchema<any, any, any> | Reference<unknown> | Lazy<any, any>>'.
//  Index signature is missing in type 'LoginFormValues'.
export const loginValidatorNoLongerValid = yup.object<LoginFormValues>({
    user: yup.string().required(),
    password: yup.string().required()
});
// Since an index signature is required, I can't constrain the generic in any meaningful way.
// So this works...
export const loginValidatorValid = yup.object({
    user: yup.string().required(),
    password: yup.string().required()
});
// And now we get to the problem: all of the below versions still pass type checking, even though
// they are not the intended/correct behavior.
export const loginValidatorTypo = yup.object({
    user: yup.string().required(),
    passworx: yup.string().required()
});
export const loginValidatorExtraField = yup.object({
    user: yup.string().required(),
    password: yup.string().required(),
    shouldNotBeHere: yup.string().required()
});
export const loginValidatorWrongValueType = yup.object({
    user: yup.string().required(),
    password: yup.number().required()
});

See how the DefinitelyTyped version makes sure that my validator type and value type (LoginFormValues) match? That’s a big deal because this is the entry point for user data into the rest of the app, so I need to be sure that my validations are correct to avoid e.g. crashing when persisting the data to my Redux store or making an API request with incorrect data.