Z
Zod2y ago
Svish

Discriminated Unions

Yeah, I was wondering about that, but can't figure out how to combine discriminated unions with other fields. Like in this case:
const Schema = z.object({
unrelated1: z.string(),
unrelated2: z.number(),
choice: z.boolean(),
ifYes: z.string(),
});
const Schema = z.object({
unrelated1: z.string(),
unrelated2: z.number(),
choice: z.boolean(),
ifYes: z.string(),
});
11 Replies
Scott Trinh
Scott Trinh2y ago
const Base = z.object({
unrelated1: z.string(),
unrelated2: z.number(),
});

const Yes = Base.extend({
choice: z.literal(true),
ifYes: z.string(),
});

const No = Base.extend({
choice: z.literal(false),
ifYes: z.date() // or whatever
});
const Base = z.object({
unrelated1: z.string(),
unrelated2: z.number(),
});

const Yes = Base.extend({
choice: z.literal(true),
ifYes: z.string(),
});

const No = Base.extend({
choice: z.literal(false),
ifYes: z.date() // or whatever
});
Something like that should work And in general, this just sorta follows how you do it in TypeScript itself in the type-language:
interface Base {
unrelated1: string;
unrelated2: number;
}

interface Yes extends Base {
choice: true;
ifYes: string;
}
// etc
interface Base {
unrelated1: string;
unrelated2: number;
}

interface Yes extends Base {
choice: true;
ifYes: string;
}
// etc
Oh, I forgot the actual union:
const Schema = z.discriminatedUnion("choice", [Yes, No]);
const Schema = z.discriminatedUnion("choice", [Yes, No]);
Svish
Svish2y ago
That makes sense. But, seems like it could become quite crazy when there's more than one conditional field in a schema though 🤔 Which I guess it would become in Typescript as well, for that matter
Scott Trinh
Scott Trinh2y ago
yeah, discriminated unions should have a single discriminant. If you need to support structural discrimination, you can use union and just make sure the order of the schemas considers that we use the first match, so if two things share similar structures, make sure the schemas are in the right order.
Svish
Svish2y ago
Actually, I suppose one solution would be to just separate it out, and then merge it together later in a transform? Will require a bit of "stitching" at the end, but could be OK I would think.
const Schema = z
.object({
text: z.string().min(0),
choice: z.discriminatedUnion('choice', [
z.object({ choice: z.literal(true), ifYes: z.string() }),
z.object({ choice: z.literal(false) }),
]),
})
.transform(({ text, choice }) => ({ text, ...choice }));
const Schema = z
.object({
text: z.string().min(0),
choice: z.discriminatedUnion('choice', [
z.object({ choice: z.literal(true), ifYes: z.string() }),
z.object({ choice: z.literal(false) }),
]),
})
.transform(({ text, choice }) => ({ text, ...choice }));
Btw, is there a way to "aim" a schema to match a certain type? Or would that just be, in this case, to stick a generic on the transform function, which would check that the return matches whatever?
Scott Trinh
Scott Trinh2y ago
Discriminated unions are really the only mechanism of "aiming" available. I think extension is probably the most popular way to deal with these kinds of things, but there isn't anything wrong with your suggestion, too.
Svish
Svish2y ago
Yeah, this would be more of a workaround just to be able to deal with the large unwieldly pension applications we have in our solution. Multipart forms with lots of yes/no questions and details depending whatever. 🥴 Working my way towards trying to replace some horribly messy magical validation and form management code written by some "clever" consultants several years ago, hehe. Trying to replace it with react-hook-form and zod, and I think it can work fairly OK, just need to create a framework of sorts around it all
Scott Trinh
Scott Trinh2y ago
yeah, i've been blessed with applications that do not have big form-driven wizards in them, but I know that's a privilege! 😂
Svish
Svish2y ago
In theory I think it should work quite easily though. Just split each wizard-page into separate schemas, then merge them all together at the end before submit. "Easy-peasy" 🤞
Scott Trinh
Scott Trinh2y ago
so easy
Svish
Svish2y ago
Thanks for the help! Really appreciate it. 🙏
Scott Trinh
Scott Trinh2y ago
of course! definitely feel free to ask any questions here. I'm a little better about checking here than in GitHub.