-
Notifications
You must be signed in to change notification settings - Fork 933
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Missing generics in create() methods #1159
Comments
open to a PR, the problem tho is the generics on the factory methods, get incorrectly inferred to object({
name: string() // ends up StringSchema<any, any>
}) which is clearly worse than their default values. I didn't have an obvious solution so deferred. Happy for some help if you want to jump in. |
You may be able to work around it with explicit overloads! |
Hmm, I can't reproduce that... this works for me:
With a new test file:
By the way, I don't think you're type checking your test files. I had to add 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 Lines 30 to 33 in 3ca0ebf
Whereas DefinitelyTyped uses The advantage of DefinitelyTyped's version is that it allows stricter value typing like this:
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 |
I'm not, at least not as part of CI, at the moment i'm manually checking them in my editor
No, intentionally so. The types here take a different approach with a different set of tradeoffs. I've definitely consulted the DT types, but overall I'm not trying to match them
this is a good example of a difference. the DT types start with a concrete type and infer a Shape that matches it, whereas the types here go the other way. You specify a concrete shape, and it derives the resulting type. There are pros/cons of both approaches, but I went with the current one because it allows producing a more correct output type. The two generics are different things here, i don't think you are losing the type safety (but happy to take a look if you have a specific example) |
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 // 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 ( |
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()
}); |
Oh, I missed that. That works for 2/3 of my negative examples: |
I'm not sure it's possible to get excess property checks here, it'd require a specific interface or mapped type neither which seem hard to use here. I can give it a shot at some point tho |
That's not quite it, it needs the generic applied to the object literal (i.e. the Anyways I think I took us on a massive tangent compared to my original concern with this issue, namely the I think where we left off was the first part of my comment here: #1159 (comment) :
I know you said this was causing overly-broad inference with Do you want to try this out, or would you rather I modify all the generic-less |
Happy to take a PR adding the right generics here. you would want to test via const foo: OptionalObjectSchema<{ a: StringSchema<string | undefined, AnyObject>}> = object({ a: string() }); is not actually ensuring that it's correct, because const foo: OptionalObjectSchema<{ a: StringSchema<string | undefined, AnyObject>}> = object({ a: null as any }); |
Oh yeah, that's true. Is https://github.com/microsoft/dtslint what you're using for |
using https://github.com/4Catalyzer/eslint-plugin-ts-expect but it's functionally the same thing, except uses eslint instead of dtslint |
Ah, thanks! I'll start working on a PR, I should have something for you to review by the end of the week, if not sooner. |
Well this is awkward. Turns out I misunderstood what was going on, I think due to some inaccuracies in the DefinitelyTyped definitions I was using previously. The way those To illustrate what I was getting confused by: // Compile error:
// Type 'StringSchema<string | undefined, Record<string, any>, string | undefined>' is not assignable to type 'BaseSchema<Maybe<string>, Record<string, any>, string>'.
// Types of property '__outputType' are incompatible.
// Type 'string | undefined' is not assignable to type 'string'.
// Type 'undefined' is not assignable to type 'string'. TS2322
export const invalid: Yup.SchemaOf<string> = Yup.string();
// Everything below compiles
export const correctA: Yup.SchemaOf<string> = Yup.string().ensure();
export const correctB: Yup.SchemaOf<string> = Yup.string().defined();
export const correctC: Yup.SchemaOf<string> = Yup.string().required(); I was trying to fix the So TLDR: no PR needed and you can close this issue - I was confused due to unsound definitions from DefinitelyTyped. Thanks so much for humoring me and helping me work through upgrading to the newest version of Yup with your type definitions. And nice job migrating to TypeScript! I did come across one unrelated issue related to the |
yeah i think that's right, generally. There is still some reason to add a generic like so: function create<T extends string>() {
return new StringSchema<T | undefined>()
}
const specific: 'hi' = create<'hi'>().default('hi') but that's a lot nichey-er of a use case |
This does work but I am still lossing the type safety inside the |
For example, see string.ts:
yup/src/string.ts
Lines 26 to 34 in 3ca0ebf
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.:Based on quickly skimming your codebase (I may have missed some), it looks like this applies to the following:
yup/src/Reference.ts
Line 13 in 3ca0ebf
yup/src/boolean.ts
Line 8 in 3ca0ebf
yup/src/date.ts
Line 16 in 3ca0ebf
yup/src/number.ts
Line 10 in 3ca0ebf
yup/src/string.ts
Line 26 in 3ca0ebf
I'm trying to upgrade from
yup@0.27.0
+@types/yup@0.26.22
to justyup@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.The text was updated successfully, but these errors were encountered: