-
-
Notifications
You must be signed in to change notification settings - Fork 198
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
Validate schema against existing type #3
Comments
I haven't had any use for it yet, but understand why it can be useful. Thank you creating this issue. |
Example to check if parsed data satisfies an existing type using the import { email, minLength, object, parse, string } from "valibot";
type LoginInput = {
email: string;
password: string;
};
const LoginSchema = object({
email: string([email()]),
password: string([minLength(8)]),
});
const LoginErrorSchema = object({
email: string([email()]),
});
let input = parse(LoginSchema, {}) satisfies LoginInput; // OK
let inputError = parse(LoginErrorSchema, {}) satisfies LoginInput; // ERROR: Property 'password' is missing in type '{ email: string; }' but required in type 'LoginInput'. I've tried to create a helper type to use the satisfies keyword with a ObjectSchema type, but quickly had to go to complex typing and there would be tons of edge cases. This monster of a type would probably be out of scope for a simple library like this one. My current recommendation would be to use the |
Thank you @Arthie for all the info! |
This is possible with import * as v from 'valibot';
type Login = {
email: string;
password: string
};
export const Login = v.object({
email: v.string(),
password: v.string(),
}) satisfies v.BaseSchema<Login>; |
@fabian-hiller Amazing, thanks! Just to confirm, does this show an error in both of the cases below?
import * as v from 'valibot';
type Login = {
email: string;
password: string
};
export const Player = v.object({
email: v.string(),
password: v.string(),
x: v.string(), // 💥 Error expected here (extra `x` property)
}) satisfies v.BaseSchema<Login>;
import * as v from 'valibot';
type Login = {
email: string;
password: string
};
export const Player = v.object({
email: v.string(),
// 💥 Error expected here (missing `password` property)
}) satisfies v.BaseSchema<Login>; |
The second works, but the first does not. The reason is that You can also convert TypeScript types to a Valibot schema on this website: https://sinclairzx81.github.io/typebox-workbench/ |
Ok, for this feature, the first one would also need to be an error to prevent excess properties being passed. This would be important if eg. adding the validated object to a database - would be problematic if inconsistent with the type. Would you consider reopening the issue and adding a feature to allow the first too? Eg. via a type annotation? |
I have no idea how to implement such a feature. Feel free to research it and share a proof of concept with me. |
Hm... maybe the generic type parameter could be passed into the https://fettblog.eu/typescript-match-the-exact-object-shape/ |
This would require a breaking change and more cumbersome code in many cases. Can you explain why such a feature would be useful? Shouldn't the Valibot schema be the source of truth for type and validation? |
I think that for some applications, the source of truth is closer to where the data lives (eg. the database), not the validation layer with the user, which in many cases is an incomplete representation of the data model. We have our source of truth as types in our database files, where SafeQL type-checks them against the SQL queries we write. |
Thank you very much! I understand the use case and can see why this is useful. I will have a look at how other schema libraries solve this in the next few weeks. |
I think this is a feature I will investigate further once Valibot v1 is available. If more people want this feature, please give this comment a thumbs up. |
This will be a game changer to my projects. I like to validate my open api types and use valibot/zod/yup to validate my partial forms in a stepper. With that check I can get a compilation error if a new field is added and my contract doesn't match the schema any more. I tested the suggested workaround but looks like at some point the |
Since we rewrote the whole library with v0.31.0, our implementation changed a bit. Just change
import * as v from 'valibot';
type Test = {
field1?: string,
field2?: string,
}
const TestSchema = v.object({
field1: v.optional(v.string()),
field2: v.optional(v.string()),
}) satisfies v.GenericSchema<Test>; |
@fabian-hiller does the If not, maybe consider implementing / copying an implementation like import { Exact } from 'ts-toolbelt/out/Function/Exact'
function exactObject<A>(x: Exact<A, {a: number, b: 2}>) {}
// ok
exactObject({} as {a: 1, b: 2})
// errors
exactObject({} as {a: 1, b: 2, c: 3})
exactObject({} as {a: 1}) Or one of the other many implementations of exact types (long thread, expand the hidden comments): |
Unfortunately, it does not. It will error if you miss a non-optional property in your schema. That's probably the most important check. But otherwise it is not 100% accurate. I am not sure if |
In my case I found that the workflow is that: valibot needs to "satisfy" the external type, but forms have extra fields and things that maybe will be map o transform into others, for example "optional" fields or transformed attributes on the way to the API like Dates . So I create the Valibot Object, and satisfies a type that I "hamer" with different |
@fabian-hiller What's your rationale behind this feeling? Did you check the playground above? It shows type checking of both excess property checks and missing property checks. |
I may be wrong. Feel free to create a proof of concept showing that a schema fails if its type does not match a predefined type. |
Additional ConcernsWith the addition of TypeScript's // ts(9010): Variable must have an explicit type annotation with --isolatedDeclarations.
export let schema = v.string() Forcing you to write types like this: export let schema: v.GenericSchema<string> = v.string() When you're being forced to maintain types and schemas separately, it's much more likely that you are going to run into the issues with nullability and additional properties. SolutionI think I've found a workable solution for enforcing the shape of object types by modifying the types in this comment for Zod: colinhacks/zod#372 (comment) Usageexport type Person = {
name: string
bio?: string
}
export let PersonSchema = v.object({
name: v.string(),
bio: v.optional(v.string()),
} satisfies Shape<Person>) Implementationimport type { GenericSchema, NullishSchema, OptionalSchema, NullableSchema, Default } from "valibot"
type ShapeNullishSchema<TSchema extends GenericSchema<unknown>> = NullishSchema<TSchema, Default<TSchema, null | undefined>>
type ShapeOptionalSchema<TSchema extends GenericSchema<unknown>> = OptionalSchema<TSchema, Default<TSchema, undefined>>
type ShapeNullableSchema<TSchema extends GenericSchema<unknown>> = NullableSchema<TSchema, Default<TSchema, null>>
type Shape<TObject extends object> = {
[TKey in keyof TObject]-?:
// Check for --strictNullChecks
null extends {} ? { "You should not use Shape<T> without using --strictNullChecks": never } :
undefined extends TObject[TKey]
? null extends TObject[TKey]
? ShapeNullishSchema<GenericSchema<TObject[TKey]>>
: ShapeOptionalSchema<GenericSchema<TObject[TKey]>>
: null extends TObject[TKey]
? ShapeNullableSchema<GenericSchema<TObject[TKey]>>
: GenericSchema<TObject[TKey]>;
}; ExamplesNo extra properties: type Person = {
name: string
}
let PersonSchema = v.object({
name: v.string(), // OK
email: v.string(), // Error
} satisfies Shape<Person>) Nullable, Nullish, Optional ( type Person = {
name: string | null
email: string | null | undefined
bio: string | undefined
website?: string
github?: string | null
}
let PersonSchema = v.object({
name: v.string(), // Error (missing nullable)
email: v.nullish(v.string()),
bio: v.optional(v.string()),
website: v.optional(v.string()),
github: v.optional(v.string()), // Error (should be nullish, not optional)
} satisfies Shape<Person>) Defaults work just fine: type Person = {
name: string | null
bio: string | null
}
let PersonSchema = v.object({
name: v.nullable(v.string(), "Anonymous"),
bio: v.nullable(v.string(), () => "I'm new here")
} satisfies Shape<Person>)
let person = v.parse(PersonSchema, { name: null, bio: null })
// >> { name: string NamingI went with the name
|
Thank you for your research. The How is the |
@jamiebuilds are there any updates on this proposed solution? |
Hi, first of all, thanks for Valibot, looks really cool 🙌
One feature that I was looking for was Yup's Ensuring a schema matches an existing type, which could look like this in Valibot:
What do you think of this feature?
The text was updated successfully, but these errors were encountered: