Z
Zodā€¢3w ago
plumbe0

Discriminated union default value on the discriminator

Discriminated union default value on the discriminator
Solution:
```ts const BaseReservation = z.object({ reservationId: z.number(), reservedBy: z.string(), });...
Jump to solution
39 Replies
plumbe0
plumbe0ā€¢3w ago
I'd like to set a default value for the discriminator of a discriminated union. is it possible and how?
Scott Trinh
Scott Trinhā€¢3w ago
meaning you want to allow it to be either undefined or some literal value coming in, and set it to the literal value by default in the undefined case?
Scott Trinh
Scott Trinhā€¢3w ago
Like this?
import { z } from "zod";

const Beep = z.object({
type: z.literal("BEEP").default("BEEP"),
beep: z.number(),
});

const Boop = z.object({
type: z.literal("BOOP").default("BOOP"),
boop: z.boolean(),
});

const BeepBoop = z.discriminatedUnion("type", [Beep, Boop]);
type BeepBoop = z.output<typeof BeepBoop>;
import { z } from "zod";

const Beep = z.object({
type: z.literal("BEEP").default("BEEP"),
beep: z.number(),
});

const Boop = z.object({
type: z.literal("BOOP").default("BOOP"),
boop: z.boolean(),
});

const BeepBoop = z.discriminatedUnion("type", [Beep, Boop]);
type BeepBoop = z.output<typeof BeepBoop>;
TypeScript Playground
TS Playground - An online editor for exploring TypeScript and JavaS...
The Playground lets you write TypeScript or JavaScript online in a safe and sharable way.
plumbe0
plumbe0ā€¢3w ago
In TS terms i was thinking like:
Type Example = {
reservationId: number,
reservedBy: Employee
} & {
haveRoom: true,
roomNumber: number
} | {
haveRoom: false
}
Type Example = {
reservationId: number,
reservedBy: Employee
} & {
haveRoom: true,
roomNumber: number
} | {
haveRoom: false
}
then when the input object to be parsed is
{
reservationId: 109,
reservedBy: {...}
}
{
reservationId: 109,
reservedBy: {...}
}
Zod's output would be
{
reservationId: 109,
reservedBy: {...},
haveRoom: false
}
{
reservationId: 109,
reservedBy: {...},
haveRoom: false
}
Scott Trinh
Scott Trinhā€¢3w ago
interface BaseReservation {
reservationId: number;
reservedBy: Employee;
}

interface WithRoom extends BaseReservation {
haveRoom: true;
roomNumber: number;
}

interface WithoutRoom extends BaseReservation {
haveRoom: false;
}

type Reservation = WithRoom | WithoutRoom;
interface BaseReservation {
reservationId: number;
reservedBy: Employee;
}

interface WithRoom extends BaseReservation {
haveRoom: true;
roomNumber: number;
}

interface WithoutRoom extends BaseReservation {
haveRoom: false;
}

type Reservation = WithRoom | WithoutRoom;
ā˜ļø that? yeah, you can do that roughly the same way in Zod:
const Employee = z.object({ type: z.literal("Employee") });

const BaseReservation = z.object({
reservationId: z.number(),
reservedBy: Employee,
});

const WithRoom = BaseReservation.extend({
haveRoom: z.literal(true).default(true),
roomNumber: z.number(),
});

const WithoutRoom = BaseReservation.extend({
haveRoom: z.literal(false).default(false),
});

const Reservation = z.discriminatedUnion("haveRoom", [WithRoom, WithoutRoom]);
const Employee = z.object({ type: z.literal("Employee") });

const BaseReservation = z.object({
reservationId: z.number(),
reservedBy: Employee,
});

const WithRoom = BaseReservation.extend({
haveRoom: z.literal(true).default(true),
roomNumber: z.number(),
});

const WithoutRoom = BaseReservation.extend({
haveRoom: z.literal(false).default(false),
});

const Reservation = z.discriminatedUnion("haveRoom", [WithRoom, WithoutRoom]);
just be careful about this: the input data is dangerously compatible--without the discriminant, the without room data is assignable to the with room data. Good thing you're parsing it and adding a discriminant šŸ˜…
plumbe0
plumbe0ā€¢3w ago
Nope, I don't think that's it. My input JSON might not have the haveRoom field at all, in which case Zod should use the default false value for it. With your schema if I pass as obj to parse:
{
reservationId: 11,
reservedBy: "Employee"
}
{
reservationId: 11,
reservedBy: "Employee"
}
it throws an invalid discriminator value for haveRoom, it should default to false
Scott Trinh
Scott Trinhā€¢3w ago
oh, right, sorry I forgot about that part, but it's the same as the suggestion from earlier. lemme edit.
plumbe0
plumbe0ā€¢3w ago
Nope, same error as before: Invalid discriminator value. Expected true | false
Scott Trinh
Scott Trinhā€¢3w ago
ah, yeah, this seems like a bug. You can parse either schema just fine, but discriminatedUnion complains. (you can use union instead, which seems to work fine) yeah, looking at the implementation, we do not actually run the schemas so we don't get the default transform, we just look up the input data at that key, and for this case, it's undefined. Unfortunately (or fortunately depending on the way you look at it), discriminatedUnion is being deprecated in favor of just a smarter union, so I doubt Colin will want to spend much effort fixing this. There's a (very) hacky fix that involves parsing with a union and then piping that to the discriminated union which gives you the "best" of both worlds, but mitigates the poor error reporting performance of union:
const Reservation = z
.union([WithRoom, WithoutRoom])
.pipe(
z.discriminatedUnion("haveRoom", [WithRoom, WithoutRoom])
);
const Reservation = z
.union([WithRoom, WithoutRoom])
.pipe(
z.discriminatedUnion("haveRoom", [WithRoom, WithoutRoom])
);
šŸ˜… This is worse performance-wise but better for error reporting, so I wouldn't do this for like an unbounded array of these objects (in that case, I might consider a custom transform) oh also, drop the z.never() that was unnecessary and actually doesn't work because undefined is not never šŸ˜…
plumbe0
plumbe0ā€¢3w ago
thanks for all the info, I might run with the "hacky" solution until v4 comes out šŸ™‚ If i was to only use union like you suggested here, how would i do it? just tried, this still doesn't work šŸ˜¢ I expected this to work, but it doesn't:
const BaseReservation = z.object({
reservationId: z.number(),
reservedBy: z.string(),
haveRoom: z.coerce.boolean().default(false),
});

const WithRoom = z.object({
haveRoom: z.literal(true),
roomNumber: z.number(),
});

const Reservation = z.union([BaseReservation, WithRoom]);

console.log(
Reservation.parse({ reservationId: 1, reservedBy: "Joe"})
);
const BaseReservation = z.object({
reservationId: z.number(),
reservedBy: z.string(),
haveRoom: z.coerce.boolean().default(false),
});

const WithRoom = z.object({
haveRoom: z.literal(true),
roomNumber: z.number(),
});

const Reservation = z.union([BaseReservation, WithRoom]);

console.log(
Reservation.parse({ reservationId: 1, reservedBy: "Joe"})
);
Scott Trinh
Scott Trinhā€¢3w ago
Yeah, that's not going to work because the no room case matches both
Solution
Scott Trinh
Scott Trinhā€¢3w ago
const BaseReservation = z.object({
reservationId: z.number(),
reservedBy: z.string(),
});

const WithoutRoom = BaseReservation.extend({
haveRoom: z.literal(false).default(false),
});

const WithRoom = BaseReservation.extend({
haveRoom: z.literal(true).default(true),
roomNumber: z.number(),
});

const Reservation = z.union([WithRoom, WithoutRoom]);

console.log(
Reservation.parse({ reservationId: 1, reservedBy: "Joe"})
);
const BaseReservation = z.object({
reservationId: z.number(),
reservedBy: z.string(),
});

const WithoutRoom = BaseReservation.extend({
haveRoom: z.literal(false).default(false),
});

const WithRoom = BaseReservation.extend({
haveRoom: z.literal(true).default(true),
roomNumber: z.number(),
});

const Reservation = z.union([WithRoom, WithoutRoom]);

console.log(
Reservation.parse({ reservationId: 1, reservedBy: "Joe"})
);
Scott Trinh
Scott Trinhā€¢3w ago
Note that the order here is very important because the without room case matches either, but it'll return the first one that matches, so if you have withoutroom first, even the withroom data will match.
plumbe0
plumbe0ā€¢3w ago
that's it! Thank you very much! You've helped me immensely This will work nice until v4 comes out šŸ™‚
Scott Trinh
Scott Trinhā€¢3w ago
Amazing. If you want the better error reporting just pipe that into a redundant discriminated union again šŸ˜…
Want results from more Discord servers?
Add your server