close
Skip to content

Commit 77a33fc

Browse files
committed
feat(v4): expose optionsMap and get(value) on ZodDiscriminatedUnion
Adds a public, type-safe way to access discriminated union members by their discriminator value, replacing the need to reach into _zod.propValues or to iterate options manually. - core: promote the internal discriminator->member map to _zod.optionsMap (lazy), typed as ReadonlyMap<Primitive, Options[number]>. - classic: add `optionsMap` getter and `get(value)` method on ZodDiscriminatedUnion. `get(value)` returns the precise member schema matching the literal value, including the case where a single member declares multiple discriminator values via z.literal([...]). - classic: `get` is installed once per prototype via _installLazyMethods; `optionsMap` is set up via util.defineLazy to match the existing pattern used for ZodObject's `shape`. Refs colinhacks#5086.
1 parent fbe8ad1 commit 77a33fc

3 files changed

Lines changed: 65 additions & 4 deletions

File tree

‎packages/zod/src/v4/classic/schemas.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1599,6 +1599,24 @@ export function xor<const T extends readonly core.SomeType[]>(
15991599
}
16001600

16011601
// ZodDiscriminatedUnion
1602+
type DiscriminatorValue<Options extends readonly core.SomeType[], Disc extends string> = {
1603+
[I in keyof Options]: Options[I] extends { _zod: { output: infer Out } }
1604+
? Out extends Record<Disc, infer V>
1605+
? V
1606+
: never
1607+
: never;
1608+
}[number];
1609+
1610+
type DiscriminatedOption<Options extends readonly core.SomeType[], Disc extends string, V> = {
1611+
[I in keyof Options]: Options[I] extends { _zod: { output: infer Out } }
1612+
? Out extends Record<Disc, infer OV>
1613+
? V extends OV
1614+
? Options[I]
1615+
: never
1616+
: never
1617+
: never;
1618+
}[number];
1619+
16021620
export interface ZodDiscriminatedUnion<
16031621
Options extends readonly core.SomeType[] = readonly core.$ZodType[],
16041622
Disc extends string = string,
@@ -1607,12 +1625,22 @@ export interface ZodDiscriminatedUnion<
16071625
"~standard": ZodStandardSchemaWithJSON<this>;
16081626
_zod: core.$ZodDiscriminatedUnionInternals<Options, Disc>;
16091627
def: core.$ZodDiscriminatedUnionDef<Options, Disc>;
1628+
/** Returns the option schema matching the given discriminator value. */
1629+
get<V extends DiscriminatorValue<Options, Disc>>(value: V): DiscriminatedOption<Options, Disc, V>;
1630+
/** A read-only map from each valid discriminator value to its corresponding option schema. */
1631+
readonly optionsMap: ReadonlyMap<DiscriminatorValue<Options, Disc>, Options[number]>;
16101632
}
16111633
export const ZodDiscriminatedUnion: core.$constructor<ZodDiscriminatedUnion> = /*@__PURE__*/ core.$constructor(
16121634
"ZodDiscriminatedUnion",
16131635
(inst, def) => {
16141636
ZodUnion.init(inst, def);
16151637
core.$ZodDiscriminatedUnion.init(inst, def);
1638+
_installLazyMethods(inst, "ZodDiscriminatedUnion", {
1639+
get(value: any) {
1640+
return this._zod.optionsMap.get(value) as any;
1641+
},
1642+
} as any);
1643+
util.defineLazy(inst, "optionsMap", () => inst._zod.optionsMap as any);
16161644
}
16171645
);
16181646

‎packages/zod/src/v4/classic/tests/discriminated-unions.test.ts‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,3 +692,35 @@ test("encode with codec discriminator", () => {
692692
const encoded = z.encode(schema, { type: "one", value: "hello" });
693693
expect(encoded).toEqual({ type: 1, value: "hello" });
694694
});
695+
696+
test("optionsMap and get(value) — by-discriminator lookup", () => {
697+
const fruit = z.object({ type: z.literal("fruit"), seeds: z.boolean() });
698+
const veg = z.object({ type: z.literal("vegetable"), leafy: z.boolean() });
699+
const schema = z.discriminatedUnion("type", [fruit, veg]);
700+
701+
expect(schema.optionsMap).toBeInstanceOf(Map);
702+
expect(Array.from(schema.optionsMap.keys())).toEqual(["fruit", "vegetable"]);
703+
expect(schema.optionsMap.get("fruit")).toBe(fruit);
704+
expect(schema.optionsMap.get("vegetable")).toBe(veg);
705+
706+
const got = schema.get("fruit");
707+
expect(got).toBe(fruit);
708+
expectTypeOf(got).toEqualTypeOf<typeof fruit>();
709+
710+
expectTypeOf(schema.get("vegetable")).toEqualTypeOf<typeof veg>();
711+
712+
// @ts-expect-error — "unknown" is not a valid discriminator value
713+
schema.get("unknown");
714+
});
715+
716+
test("get(value) — single member with multiple discriminator values", () => {
717+
const a = z.object({ type: z.literal(["x", "y"]), payload: z.string() });
718+
const b = z.object({ type: z.literal("z"), payload: z.number() });
719+
const schema = z.discriminatedUnion("type", [a, b]);
720+
721+
expect(schema.get("x")).toBe(a);
722+
expect(schema.get("y")).toBe(a);
723+
expect(schema.get("z")).toBe(b);
724+
expectTypeOf(schema.get("x")).toEqualTypeOf<typeof a>();
725+
expectTypeOf(schema.get("z")).toEqualTypeOf<typeof b>();
726+
});

‎packages/zod/src/v4/core/schemas.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2348,6 +2348,7 @@ export interface $ZodDiscriminatedUnionInternals<
23482348
> extends $ZodUnionInternals<Options> {
23492349
def: $ZodDiscriminatedUnionDef<Options, Disc>;
23502350
propValues: util.PropValues;
2351+
optionsMap: ReadonlyMap<util.Primitive, Options[number]>;
23512352
}
23522353

23532354
export interface $ZodDiscriminatedUnion<
@@ -2381,7 +2382,7 @@ export const $ZodDiscriminatedUnion: core.$constructor<$ZodDiscriminatedUnion> =
23812382
return propValues;
23822383
});
23832384

2384-
const disc = util.cached(() => {
2385+
util.defineLazy(inst._zod, "optionsMap", () => {
23852386
const opts = def.options as $ZodTypeDiscriminable[];
23862387
const map: Map<util.Primitive, $ZodType> = new Map();
23872388
for (const o of opts) {
@@ -2395,7 +2396,7 @@ export const $ZodDiscriminatedUnion: core.$constructor<$ZodDiscriminatedUnion> =
23952396
map.set(v, o);
23962397
}
23972398
}
2398-
return map;
2399+
return map as ReadonlyMap<util.Primitive, $ZodType>;
23992400
});
24002401

24012402
inst._zod.parse = (payload, ctx) => {
@@ -2411,7 +2412,7 @@ export const $ZodDiscriminatedUnion: core.$constructor<$ZodDiscriminatedUnion> =
24112412
return payload;
24122413
}
24132414

2414-
const opt = disc.value.get(input?.[def.discriminator] as any);
2415+
const opt = inst._zod.optionsMap.get(input?.[def.discriminator] as any);
24152416
if (opt) {
24162417
return opt._zod.run(payload, ctx) as any;
24172418
}
@@ -2430,7 +2431,7 @@ export const $ZodDiscriminatedUnion: core.$constructor<$ZodDiscriminatedUnion> =
24302431
errors: [],
24312432
note: "No matching discriminator",
24322433
discriminator: def.discriminator,
2433-
options: Array.from(disc.value.keys()),
2434+
options: Array.from(inst._zod.optionsMap.keys()),
24342435
input,
24352436
path: [def.discriminator],
24362437
inst,

0 commit comments

Comments
 (0)