Variant schemas with shared elements #1405
-
|
Hi there! I have a use case that is mostly but not entirely a discriminated union, namely that my schemas share some keys but not others. I've been using // shared across all types in variant
const CommonSchema = v.object({
species: v.string(),
});
const CatSchema = v.object({
...CommonSchema.entries,
type: v.literal('cat'),
cat: v.object({
isLongHair: v.boolean(),
}),
});
const DogSchema = v.object({
...CommonSchema.entries,
type: v.literal('dog'),
dog: v.object({
breed: v.string(),
group: v.string(),
}),
});
const BirdSchema = v.object({
...CommonSchema.entries,
type: v.literal('bird'),
bird: v.object({
canFly: v.boolean(),
}),
});
const AnimalSchema = v.variant('type', [
CatSchema,
DogSchema,
BirdSchema,
]);
type Animal = v.InferOutput<typeof AnimalSchema>;The generated type for type Animal = {
type: "cat";
cat: { isLongHair: boolean };
species: string;
} | {
type: "dog";
dog: { group: string };
species: string;
} | {
type: "bird";
bird: { canFly: boolean };
species: string;
};Is there a way to do this that results in a type more like the following? type Animal = {
species: string;
} & (
| {
type: 'cat';
cat: { isLongHair: boolean };
}
| {
type: 'dog';
dog: { group: string };
}
| {
type: 'bird';
bird: { canFly: boolean };
}
);This would greatly reduce the size of the type (as my schemas have well over a dozen shared properties), while still preserving the benefits of the discriminated union. I'm basically thinking of something like this, where the third argument is a common/shared schema that they would all use: const AnimalSchema = v.variant(
'type',
[CatSchema, DogSchema, BirdSchema],
CommonSchema,
);Unless there's a way to already do this that I'm missing? Thank you! |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 1 reply
-
|
Hi @alexwgraves! I'm Dosu and I’m helping the valibot team. Valibot doesn't currently support factoring out shared properties in variant schemas via a third argument or built-in feature in To achieve the intersection type you want (shared properties factored out), the recommended pattern is to use Here's how you can do it: const CommonSchema = v.object({
species: v.string(),
});
const CatSchema = v.object({
type: v.literal('cat'),
cat: v.object({ isLongHair: v.boolean() }),
});
const DogSchema = v.object({
type: v.literal('dog'),
dog: v.object({ breed: v.string(), group: v.string() }),
});
const BirdSchema = v.object({
type: v.literal('bird'),
bird: v.object({ canFly: v.boolean() }),
});
const AnimalVariant = v.variant('type', [
CatSchema,
DogSchema,
BirdSchema,
]);
const AnimalSchema = v.intersect([CommonSchema, AnimalVariant]);
type Animal = v.InferOutput<typeof AnimalSchema>;This results in the desired type structure: type Animal = {
species: string;
} & (
| { type: 'cat'; cat: { isLongHair: boolean } }
| { type: 'dog'; dog: { breed: string; group: string } }
| { type: 'bird'; bird: { canFly: boolean } }
);A couple of caveats: if you have conflicting keys between the shared schema and any variant, There’s no indication in the docs or codebase that To reply, just mention @dosu. Docs are dead. Just use Dosu. |
Beta Was this translation helpful? Give feedback.
-
|
My case was a bit complicated by variant-specific checks, but I was able to add those checks on after creating an intersect schema by using |
Beta Was this translation helpful? Give feedback.
Hi @alexwgraves! I'm Dosu and I’m helping the valibot team.
Valibot doesn't currently support factoring out shared properties in variant schemas via a third argument or built-in feature in
v.variant—each variant option must include all its properties, so the union type repeats shared keys and can get verbose as you add more variants source.To achieve the intersection type you want (shared properties factored out), the recommended pattern is to use
v.intersectto combine your shared schema with the variant schema. This produces a type like{shared} & (variant union)and helps reduce redundancy and TypeScript complexity source.Here's how you can do it: