Repository navigation
Replies: 1 comment
|
I'd love to see this in Zod proper, but in the meantime, I came up with a small utility for this: import { z } from "zod";
type Primitive = string | number | symbol | bigint | boolean | null | undefined;
function isPrimitive(v: unknown): v is Primitive {
return typeof v === "string" || typeof v === "number" || typeof v === "symbol" || typeof v === "bigint" || typeof v === "boolean" || v == null;
};
export function orFallback<T extends z.ZodDiscriminatedUnion<string, any>, U>(
union: T,
fallback: (value: Record<string, unknown>) => U
): z.ZodType<z.output<T> | U, z.ZodTypeDef, Record<string, unknown>> {
const inOptionsMap = (k: unknown) => isPrimitive(k) && union.optionsMap.has(k);
return z.record(z.unknown()).transform((value, ctx) => {
if (
union.discriminator in value &&
inOptionsMap(value[union.discriminator])
) {
const result = union.safeParse(value);
if (result.success) {
return result.data;
} else {
for (const issue of result.error.issues) {
ctx.addIssue(issue);
}
}
} else {
return fallback(value);
}
});
}Here's what usage looks like: // The raw discriminated union we want to parse
const ThingStrict = z.discriminatedUnion("type", [
z.object({ type: z.literal("foo"), foo: z.string() }),
z.object({ type: z.literal("bar"), bar: z.number() }),
z.object({ type: z.literal("baz"), baz: z.boolean() }),
]);
// Our union that returns a fallback if no discriminant matches
const Thing = orFallback(ThingStrict, (value) => ({ type: "unknown", value } as const));
// Parsing examples:
Thing.parse({ type: "foo", foo: "abc" });
// Valid discriminant, parses like normal
// { type: "foo", foo: "abc" }
Thing.parse({ type: "asdf" });
// Unknown discriminant, parses to the fallback
// { type: "unknown", value: { type: "asdf" } }
Thing.safeParse({ type: "foo", foo: 123 });
// Valid discriminant, but the option does not parse properly
// { success: false, error: {errors: [{message: "Expected string, received number"}]}} |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment

Uh oh!
There was an error while loading. Please reload this page.
I have data coming in which includes an array of objects of this (simplified) form:
There are a lot of possible
RequestCodestrings, and I can discriminate based on them and know the types ofValueand the other properties I've omitted. However, I don't know all the possible values, and more could be added at any time. This means I can't make an exhaustive discriminated union. I also don't care about them all anyway.The ones I care about look like this:
I can put these in a union or discriminated union, but I also need to accept all the other types I don't care about.
When I process them I need to narrow the type.
My first attempt was like this:
But this doesn't narrow like I wanted it to. TS doesn't know for example in the
type1case if it's atype1or the generic, and sopoint.Valuehas typenumber | string.Next I tried without Zod, typing
RequestCodeasExclude<string, Type1["RequestCode"] | Type2["RequestCode"]>, but this had the same problem.So at the moment I am using type guards:
This works but I'm going to have dozens of these types and it's a lot of boilerplate.
Any suggestions on a better way to do this?
If I'm on the right track already with the type guards, is there at least a way to reduce the amount of boilerplate?
Like if I declare all the data point types as properties of an object (
{ type1: z.object(...), type2: z.object(...), ... }) is there maybe a terse way to generate all the type guards via a map from that in a way that TS understands their types, and then beyond that to feed them all to Zod for the union? I don't want to have to add code in 3 places every time I need to handle another of these data types.All reactions