Z
Zodā€¢3mo 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
plumbe0OPā€¢3mo 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ā€¢3mo 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ā€¢3mo 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
plumbe0OPā€¢3mo 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ā€¢3mo 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
plumbe0OPā€¢3mo 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ā€¢3mo ago
oh, right, sorry I forgot about that part, but it's the same as the suggestion from earlier. lemme edit.
plumbe0
plumbe0OPā€¢3mo ago
Nope, same error as before: Invalid discriminator value. Expected true | false
Scott Trinh
Scott Trinhā€¢3mo 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
plumbe0OPā€¢3mo 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ā€¢3mo ago
Yeah, that's not going to work because the no room case matches both
Solution
Scott Trinh
Scott Trinhā€¢3mo 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ā€¢3mo 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
plumbe0OPā€¢3mo 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ā€¢3mo ago
Amazing. If you want the better error reporting just pipe that into a redundant discriminated union again šŸ˜…
plumbe0
plumbe0OPā€¢3mo ago
would this work for more with/without cases? i can just extend BaseSchema more times and then union all of those?
Scott Trinh
Scott Trinhā€¢3mo ago
that's right! Just watch out for assignability and ordering if the objects are very similar to each other.
plumbe0
plumbe0OPā€¢2mo ago
This worked at last:
const Schema = z
.union([WithB, WithoutB])
.and(z.union([WithC, WithoutC]))
.and(z.union([WithD, WithoutD]));
const Schema = z
.union([WithB, WithoutB])
.and(z.union([WithC, WithoutC]))
.and(z.union([WithD, WithoutD]));
plumbe0
plumbe0OPā€¢2mo ago
@Scott Trinh sorry to bother you again, how would i pipe this to the discriminated union(s?) to get the better error reporting? Thanks
Scott Trinh
Scott Trinhā€¢2mo ago
const Schema = z
.union([WithB, WithoutB]).pipe(z.discriminatedUnion("someProp", [WithB, WithoutB])
.and(z.union([WithC, WithoutC]).pipe(z.discriminatedUnion("someProp", [WithC, WithoutC]))
.and(z.union([WithD, WithoutD]).pipe(z.discriminatedUnion("someProp", [WithD, WithoutD])))
const Schema = z
.union([WithB, WithoutB]).pipe(z.discriminatedUnion("someProp", [WithB, WithoutB])
.and(z.union([WithC, WithoutC]).pipe(z.discriminatedUnion("someProp", [WithC, WithoutC]))
.and(z.union([WithD, WithoutD]).pipe(z.discriminatedUnion("someProp", [WithD, WithoutD])))
plumbe0
plumbe0OPā€¢2mo ago
Thanks, i get this at runtime though: error: A discriminator value for key haveA could not be extracted from all schema options
Scott Trinh
Scott Trinhā€¢2mo ago
oh, true, I guess you'd need to redefine the schemas to have a more strict version that doesn't need to handle the no discriminant case. Can you provide an exact reproduction here, maybe as a TypeScript Playground?
plumbe0
plumbe0OPā€¢2mo ago
sorry for the huge link, I don't know if discord supports markdown
Scott Trinh
Scott Trinhā€¢2mo ago
ha, no problem. it does support markdown links like github (e.g. [description](href)) OK, the issue here is that your discriminants are not defined as literals, but rather as coerced booleans. This works:
const Base = z.object({
id: z.number().optional(),
reservedBy: z.string().min(1),
notes: z.string().optional(), // TODO if notes === "" delete it
});

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

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

const NotCheckedIn = Base.extend({
isCheckedIn: z.literal(false).default(false),
});

const CheckedIn = Base.extend({
isCheckedIn: z.literal(true).default(true),
checkedInAt: z.string().datetime(),
checkedInBy: z.string().min(1),
});

const NotAvailable = Base.extend({
isUnderMaintainance: z.literal(true).default(true),
});

const MaybeAvailable = Base.extend({
isUnderMaintainance: z.literal(false).default(false),
isBookable: z.coerce.boolean().default(false),
});
const Base = z.object({
id: z.number().optional(),
reservedBy: z.string().min(1),
notes: z.string().optional(), // TODO if notes === "" delete it
});

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

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

const NotCheckedIn = Base.extend({
isCheckedIn: z.literal(false).default(false),
});

const CheckedIn = Base.extend({
isCheckedIn: z.literal(true).default(true),
checkedInAt: z.string().datetime(),
checkedInBy: z.string().min(1),
});

const NotAvailable = Base.extend({
isUnderMaintainance: z.literal(true).default(true),
});

const MaybeAvailable = Base.extend({
isUnderMaintainance: z.literal(false).default(false),
isBookable: z.coerce.boolean().default(false),
});
If you're trying to handle possible string-y discriminant here, you'll need to probably use pipe here, but I don't think you need to worry about that unless you're parsing form encoded data or URL search parameters. Also, not everything needs to extend Base, so here's a working (I think?) example:
const WithoutRoom = z.object({
haveRoom: z.literal(false).default(false),
});

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

const NotCheckedIn = z.object({
isCheckedIn: z.literal(false).default(false),
});

const CheckedIn = z.object({
isCheckedIn: z.literal(true).default(true),
checkedInAt: z.string().datetime(),
checkedInBy: z.string().min(1),
});

const NotAvailable = z.object({
isUnderMaintainance: z.literal(true).default(true),
});

const MaybeAvailable = z.object({
isUnderMaintainance: z.literal(false).default(false),
isBookable: z.coerce.boolean().default(false),
});

export const ReservationSchema = Base.and(
z
.union([WithRoom, WithoutRoom])
.pipe(z.discriminatedUnion("haveRoom", [WithRoom, WithoutRoom]))
.and(
z
.union([CheckedIn, NotCheckedIn])
.pipe(z.discriminatedUnion("isCheckedIn", [CheckedIn, NotCheckedIn]))
)
.and(
z
.union([NotAvailable, MaybeAvailable])
.pipe(
z.discriminatedUnion("isUnderMaintainance", [
NotAvailable,
MaybeAvailable,
])
)
)
);
const WithoutRoom = z.object({
haveRoom: z.literal(false).default(false),
});

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

const NotCheckedIn = z.object({
isCheckedIn: z.literal(false).default(false),
});

const CheckedIn = z.object({
isCheckedIn: z.literal(true).default(true),
checkedInAt: z.string().datetime(),
checkedInBy: z.string().min(1),
});

const NotAvailable = z.object({
isUnderMaintainance: z.literal(true).default(true),
});

const MaybeAvailable = z.object({
isUnderMaintainance: z.literal(false).default(false),
isBookable: z.coerce.boolean().default(false),
});

export const ReservationSchema = Base.and(
z
.union([WithRoom, WithoutRoom])
.pipe(z.discriminatedUnion("haveRoom", [WithRoom, WithoutRoom]))
.and(
z
.union([CheckedIn, NotCheckedIn])
.pipe(z.discriminatedUnion("isCheckedIn", [CheckedIn, NotCheckedIn]))
)
.and(
z
.union([NotAvailable, MaybeAvailable])
.pipe(
z.discriminatedUnion("isUnderMaintainance", [
NotAvailable,
MaybeAvailable,
])
)
)
);
Scott Trinh
Scott Trinhā€¢2mo ago
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
plumbe0OPā€¢2mo ago
thank you very much for support. the problem with using z.literal() instead of z.coerce.boolean() is that the haveX properties come from parsing FormData checkboxes, and they come with the default "on" value... so maybe it's better to z.literal("on").pipe(z.literal(true)) to transform them?
Scott Trinh
Scott Trinhā€¢2mo ago
pipe doesn't work like that, it would try to check to see if the literal value "on" was the literal value true Is the input data exactly: "on" | "off" or are there other cases like undefined? I think it would be helpful to share some valid input data because this whole time I was thinking you were dealing with these keys being missing but if they're there, that's a completely different thing šŸ˜…
plumbe0
plumbe0OPā€¢2mo ago
in short the input comes either from a form or from a sqlite db. haveA, haveB, haveC are checkboxes so as per HTML standard if they're not checked they are not sent on form submit; if they're checked the key/value pair is sent and the default value is the string "on" if not otherwise specified. I'll send some input examples as soon as i get back to my computer
Scott Trinh
Scott Trinhā€¢2mo ago
ahh, and you're trying to use the same schema to process data from sqlite (which uses a boolean here?) and the form?
plumbe0
plumbe0OPā€¢2mo ago
sqlite does not have boolean, uses 0 or 1 integer. so z.coerce.boolean() was working fine. so all in all haveX: "on" | 0 | 1 | undefined so zod should parse true if value is "on" or 1 , false if 0 or undefined I'm assuming typescript's undefined means the property might be missing on the input data so the equivalent of zod's .optional()
Scott Trinh
Scott Trinhā€¢2mo ago
yeah, that's roughly correct (although strict property access means the semantics here are a little bit off until we fix that in zod 4) Ok, that's helpful. I'd use this:
const trueish = z.union([
z.literal("on"),
z.literal(1),
]).transform(() => true);

const falseish = z.union([
z.literal(0),
z.undefined(),
]).transform(() => false);
const trueish = z.union([
z.literal("on"),
z.literal(1),
]).transform(() => true);

const falseish = z.union([
z.literal(0),
z.undefined(),
]).transform(() => false);
And then use those for the schemas like: well, I guess this is a little more complicated than that because discriminatedUnion doesn't like a union discriminant even though the output is a constant literal. I think there are two general issues: 1. discriminatedUnion is broken for anything but the most trivial case. 2. Trying to write a schema that work across multiple boundary points is tough. The first point is being addressed in an upcoming version of Zod. The second point is a little more subtle. I really want to write a blog post about this, but in essence, I think it's usually prudent to write 1+n schemas where n is the number of different I/O boundaries you might have, so in your case: 3. 1. Domain object (the schema for the object as you use it in your actual code) 2. Encoding schema for form data 3. Encoding/decoding schema for sqlite data You can skip the Domain Object one usually, but I find it a nice thing to have within your actual code that's the real type, and you can use it to pipe the other schemas into it to ensure they're fully 100% compliant. I'll take a swing at doing this for your example schema and see if it works well.
Scott Trinh
Scott Trinhā€¢2mo ago
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.
Scott Trinh
Scott Trinhā€¢2mo ago
const Base = z.object({
id: z.number().optional(),
reservedBy: z.string().min(1),
notes: z.string().optional(), // TODO if notes === "" delete it
});

const WithoutRoom = z.object({
haveRoom: z.literal(false),
});

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

const NotCheckedIn = z.object({
isCheckedIn: z.literal(false),
});

const CheckedIn = z.object({
isCheckedIn: z.literal(true),
checkedInAt: z.string().datetime(),
checkedInBy: z.string().min(1),
});

const NotAvailable = z.object({
isUnderMaintainance: z.literal(true),
});

const MaybeAvailable = z.object({
isUnderMaintainance: z.literal(false),
isBookable: z.literal(false),
});

export const ReservationSchema = Base.and(
z
.discriminatedUnion("haveRoom", [WithRoom, WithoutRoom])
.and(z.discriminatedUnion("isCheckedIn", [CheckedIn, NotCheckedIn]))
.and(
z.discriminatedUnion("isUnderMaintainance", [
NotAvailable,
MaybeAvailable,
])
)
);

export type Reservation = z.output<typeof ReservationSchema>;
const Base = z.object({
id: z.number().optional(),
reservedBy: z.string().min(1),
notes: z.string().optional(), // TODO if notes === "" delete it
});

const WithoutRoom = z.object({
haveRoom: z.literal(false),
});

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

const NotCheckedIn = z.object({
isCheckedIn: z.literal(false),
});

const CheckedIn = z.object({
isCheckedIn: z.literal(true),
checkedInAt: z.string().datetime(),
checkedInBy: z.string().min(1),
});

const NotAvailable = z.object({
isUnderMaintainance: z.literal(true),
});

const MaybeAvailable = z.object({
isUnderMaintainance: z.literal(false),
isBookable: z.literal(false),
});

export const ReservationSchema = Base.and(
z
.discriminatedUnion("haveRoom", [WithRoom, WithoutRoom])
.and(z.discriminatedUnion("isCheckedIn", [CheckedIn, NotCheckedIn]))
.and(
z.discriminatedUnion("isUnderMaintainance", [
NotAvailable,
MaybeAvailable,
])
)
);

export type Reservation = z.output<typeof ReservationSchema>;
export const Reservation__FormData = Base.and(
z
.union([
WithRoom.extend({
haveRoom: z.literal("on").transform(() => true),
}),
WithoutRoom.extend({
haveRoom: z.undefined().transform(() => false),
}),
])
.and(
z.union([
CheckedIn.extend({
isCheckedIn: z.literal("on").transform(() => true),
}),
NotCheckedIn.extend({
isCheckedIn: z.undefined().transform(() => false),
}),
])
)
.and(
z.union([
NotAvailable.extend({
isUnderMaintainance: z.literal("on").transform(() => true),
}),
MaybeAvailable.extend({
isUnderMaintainance: z.undefined().transform(() => false),
}),
])
)
).pipe(ReservationSchema);

type Reservation__FormData = z.input<typeof Reservation__FormData>;

export const Reservation__Sqlite = Base.and(
z
.union([
WithRoom.extend({
haveRoom: z.literal(1).transform(() => true),
}),
WithoutRoom.extend({
haveRoom: z.literal(0).transform(() => false),
}),
])
.and(
z.union([
CheckedIn.extend({
isCheckedIn: z.literal(1).transform(() => true),
}),
NotCheckedIn.extend({
isCheckedIn: z.literal(0).transform(() => false),
}),
])
)
.and(
z.union([
NotAvailable.extend({
isUnderMaintainance: z.literal(1).transform(() => true),
}),
MaybeAvailable.extend({
isUnderMaintainance: z.literal(0).transform(() => false),
}),
])
)
).pipe(ReservationSchema);

type Reservation__Sqlite = z.input<typeof Reservation__Sqlite>;
export const Reservation__FormData = Base.and(
z
.union([
WithRoom.extend({
haveRoom: z.literal("on").transform(() => true),
}),
WithoutRoom.extend({
haveRoom: z.undefined().transform(() => false),
}),
])
.and(
z.union([
CheckedIn.extend({
isCheckedIn: z.literal("on").transform(() => true),
}),
NotCheckedIn.extend({
isCheckedIn: z.undefined().transform(() => false),
}),
])
)
.and(
z.union([
NotAvailable.extend({
isUnderMaintainance: z.literal("on").transform(() => true),
}),
MaybeAvailable.extend({
isUnderMaintainance: z.undefined().transform(() => false),
}),
])
)
).pipe(ReservationSchema);

type Reservation__FormData = z.input<typeof Reservation__FormData>;

export const Reservation__Sqlite = Base.and(
z
.union([
WithRoom.extend({
haveRoom: z.literal(1).transform(() => true),
}),
WithoutRoom.extend({
haveRoom: z.literal(0).transform(() => false),
}),
])
.and(
z.union([
CheckedIn.extend({
isCheckedIn: z.literal(1).transform(() => true),
}),
NotCheckedIn.extend({
isCheckedIn: z.literal(0).transform(() => false),
}),
])
)
.and(
z.union([
NotAvailable.extend({
isUnderMaintainance: z.literal(1).transform(() => true),
}),
MaybeAvailable.extend({
isUnderMaintainance: z.literal(0).transform(() => false),
}),
])
)
).pipe(ReservationSchema);

type Reservation__Sqlite = z.input<typeof Reservation__Sqlite>;
Note that I've made type aliases for the input type of each boundary schema so that you have a type for what the input data actually looks like that you can use for tests, etc. i've left out the decoding back to Sqlite transformation (true -> 1, etc) as that is (hopefully!) being done by the database driver already, but feel free to make yourself a schema that is an inverse there.
plumbe0
plumbe0OPā€¢2mo ago
That's wild, thank you! I'll try to integrate this huge help into my code. I would gladly read your blog post about this šŸ™‚ It seems to work for my initial testing... But I'm having some trouble with the UI: my form page backend uses safeParse() and if !result.success returns the form page pre-populated with result.data which is... inconsistent, because i can check the checkbox for haveRoom and not choose any valid option from the <select> associated to roomNumber, in which case the backend receives an incosistent object with any of the schemas defined. Is it a common problem? How is it usually solved? Thanks again for the incredible patience and help you've given me
Scott Trinh
Scott Trinhā€¢2mo ago
But I'm having some trouble with the UI: my form page backend uses safeParse() and if !result.success returns the form page pre-populated with result.data which is... inconsistent
I'm not sure I follow this. result.data should be empty if result.success === false
plumbe0
plumbe0OPā€¢2mo ago
Ah, I'm sorry, I'm using the validator middleware of Hono web framework. It uses .spa() under the hood and i think returns the input as result.data even if result.success === false. I guess I'm on my own for this one, as zod does not return a "partial" parsed object
Scott Trinh
Scott Trinhā€¢2mo ago
yeah šŸ˜… I feel like that's a bad idea, but some people have actually asked for this behavior (partial parsing)
plumbe0
plumbe0OPā€¢2mo ago
fixed with god's intended language: HTML. multiple options can be selected but only the last one will be truly selected (its text will appear in the UI and its value will be sent) so i keep the default invalid option "please choose ..." always selected and then if there is a previous valid selection it will automatically override, otherwise it will fallback to the default disabled option
Want results from more Discord servers?
Add your server