valibot
valibot copied to clipboard
Variant / Discriminated union based on a subkey
Let’s say I’m validating data of this shape:
{
"id": "4567",
"value": {
"streetAddress": "14720 Honore Avenue",
"secondaryAddress": "",
"city": "Harvey",
"postalCode": "60426",
"state": "IL",
"country": "US",
},
"fieldConfig": {
"id": "1234",
"key": "address",
"type": "address",
"label": "Address",
"options": {
"optionA": true
},
},
},
The value shape is dependent on the field type.
Is there an ergonomic way to validate such shape?
Something like this (not working, just to give an idea):
const FieldSchema = v.variant('fieldConfig.type', [
AddressFieldSchema,
...
])
I don’t insist on a dot notation API, if there’s an ergonomic way to do it already, please do tell.
This has been brought up in Zod as well: https://github.com/colinhacks/zod/issues/1868
I will consider supporting an API like this in the long run:
const FieldSchema = v.variant(['fieldConfig', 'type'], [
AddressFieldSchema,
...
])
Would you be interested in investigating the implementation? The hard part will be getting the types right and implementing the validation without adding a lot of code to the bundle size.
In the meantime you can just write:
const FieldSchema = v.variant('fieldConfig', [
AddressFieldSchema,
...
])
With this workaround, the types aren’t narrowed properly though
If this is the case, it is a limitation of TypeScript and not Valibot. Can you try defining such a type with pure TypeScript types?
Surely possible with typescript by using template literal types and accessing object deeply. react-hook-form has something similar.
However types performance is absolute tragedy with such usage.
@fabian-hiller just tried it and indeed it’s a TS limitation... Here is a reproduction
I suppose there’s no difference between v.variant & v.union in this case, is there @fabian-hiller?
The best thing to do here is just use type guards, I think
Managed to define such type with Typescript:
https://www.typescriptlang.org/play/?#code/C4TwDgpgBAEghgZwNIRAHgPICMBWUIAewEAdgCYJQD2uEAxsADRQoj5GkVQLABOAliQDmAPigBeFqnbFylANaoqAMyjY8Afih8ArtABcUZXAA2CCAG4AUFdCQoAJXoSoAbytQoAbXmGeA4QBdPz5BISgAH0dnKJ0SeRIqAHcSKwBfGztoAHEIYABlUOEAIRAABThgAAtMXBlOSic6Zgrq+rluItEXVqr2rgADABJXQWUIXigAGTSAOhGxicc0gY8oLSn+hSVVdTXPLXUvKcCt6Lp9z3WoXIKu0t7anGPTgDJz5gcRS89DI5Ozv4wj8rodcC8Qb8oCQIAA3CaXQww+G8NaGXpnRQgFRqXCXMHPXqnQiyLhA4QggleIkgpFwhFXKB0lHWWzgaAAYSoAFswHBeBBCgEhA9KjV1Gcmi0xYCuswAKIESAMCBkABqpj0suFYkkGJJDSgw1GJHGkxm8xNZuWqwO00xO1xOHxTpekvolKgXN5-MF93KYqebveUscCqV9GI6s1EG+jKh-2JHA65KEkOuibOiuVUY1Ji1Wl0BiMpnM6eZDKuFdRUP1ya4WJxe0ZVKJ2uB8Yz4LbBo62cjqrzBe0vC1hmMZggtOh9JrVZnLMy7KgADFBNGBHASMA0AAVd3NKB10mUVPhnODmPt4S6qBrfe9rhbtgu718gVCsKi6p76XVc8DtG+axmcRZ3p2Wj7tOyJLIiC4TKybL2AAclQZAQBqm7bpQkjuJ4ZCVHAhiuFA3KCEOEBIjo3JYAhpFwAQFFUTRSwZJ4iTocR2jsoYABEJDUbRvC8VAGRpJEbhrARwBEW4pGCFMpBCNUzFCRY9EEIpwgqdCgmsWsHGUXJWR8amIliTYSHQLuEA8AAgi4a7kJh-BbjueFQNJskkWRJBMbpLG8Op3IMf5AmBaJBloUZJEmVA-F6cJkXiVEHleVxvlacpVSqXRIWaUpOnhUJkXsdFXFxbxZnJcw-HRbMWS8bVZkiOpAD0bVQDoYDaFQUBVBM0D8MAUD8JQcBYCY0DAH1iy8G1yjrkAA
It's possible to deep-check value with path and expected value, and get required value.
But I can't get function to do it's job
const node = { data: { minValue: 5, maxValue: 7 }, node: { type: "number" } } as NodeVariants;
node.node.type; // string | number
const deepVariant = <Obj extends Rec, Path extends string, ExpectedValue extends GetStringByPath<Obj, Path>>(obj: Obj, path: Path): FindVariant<Obj, Path, ExpectedValue> => {
return obj as FindVariant<Obj, Path, ExpectedValue>
}
const n = deepVariant(node, "node.type");
n.node.type // number
this one doesn't narrow down
I've "semi" working (partial replication of variants, no nested variants yet, and few other things skipped for now), and it would be possible to get an union type out of it.
However, to even use that discriminated union, it would probably require helper function - but in best case scenario it would be a matcher function (like a switch)
https://github.com/fabian-hiller/valibot/pull/1112
- I'm pretty positive, that due to TS limitations, we can't use that value without
matchVariation - Still have to check performance.
I suppose there’s no difference between v.variant & v.union in this case, is there @fabian-hiller?
I would probably still use variant as it may be faster and return better issues. We will ship a performance improvement for this cases with the next version. See PR #1110.
@muningis I am very sorry for being so slow to respond. I have almost no time at the moment. I will try to catch up in the next few days.
No worries. It's just experimental POC PR just to even get idea if it's possible.
I’ve been thinking about it more lately, and I think implementing something like distributed union with a nested discriminant would be swimming against the tide.
It doesn’t work in TypeScript yet, and there’s an open issue for it as well. I’d say it would be wisest to wait until TypeScript expands the narrowing rules.
The feature itself — in my case — would just be making up for a bad API design, and creating type guards manually is still a great option.
@fabian-hiller feel free to close this issue.
I'd also love to have something for this. Currently using pattern:
const propertiesSchema = variant('name', [
looseObject({
name: literal('body_type'),
value: picklist([
// ...
])
}),
looseObject({
name: literal('brand'),
value: string()
}),
looseObject({
name: literal('condition'),
value: picklist([
// ...
])
})
])
exporting separate types:
export type BodyType = Extract<InferOutput<typeof propertiesSchema>, { name: 'body_type' }>
export type Brand = Extract<InferOutput<typeof propertiesSchema>, { name: 'brand' }>
using:
const bodyType = (car.properties.find(({ name }) => name === 'body_type') as BodyType).value
which feels so dirty ðŸ˜
@vladshcherbin I think your code is not related to this issue or am I wrong? This seems more like a TS inference limitation when using array.find().
@vladshcherbin It's working without deconstruction. Tested on [email protected]
Not working 🚫:
const bodyType = car.properties.find(({ name }) => name === 'body_type')?.value
// string | undefined
Working ✅:
const bodyType = car.properties.find((property) => property.name === 'body_type')?.value
// "picklist_1" | "picklist_2" | undefined
@vladshcherbin - that would require full code shown. However as BodyType is already widening the type.
Hi, @selrond. I'm Dosu, and I'm helping the Valibot team manage their backlog. I'm marking this issue as stale.
Issue Summary
- The issue involves finding an ergonomic way to validate data structures where
valueshape depends onfieldConfig.type. - @fabian-hiller suggested an API and workaround, but you noted TypeScript limitations in type narrowing.
- @muningis explored solutions but encountered performance issues.
- You suggested waiting for future TypeScript updates due to current limitations.
- @fabian-hiller mentioned potential performance improvements in upcoming TypeScript versions.
Next Steps
- Please let me know if this issue is still relevant with the latest version of Valibot by commenting here.
- If there is no further activity, this issue will be automatically closed in 30 days.
Thank you for your understanding and contribution!