Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions typescript/src/zod/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,11 @@ const enum TypePrecedence {
function getTypePrecedence(type: z.ZodType): TypePrecedence {
switch (getTypeKind(type)) {
case "enum":
case "optional": // emitted as `T | undefined`
case "union": // covers both z.union() and z.discriminatedUnion() — Zod v4 merged discriminated unions into the regular union type kind ("ZodDiscriminatedUnion" in v3)
return TypePrecedence.Union;
case "literal": // a multi-value literal is emitted as a union of its values
return (type._zod.def as z.core.$ZodLiteralDef<z.core.util.Literal>).values.length > 1 ? TypePrecedence.Union : TypePrecedence.Object;
case "intersection":
return TypePrecedence.Intersection;
}
Expand Down
37 changes: 37 additions & 0 deletions typescript/test/zod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,18 @@ describe("getZodSchemaAsTypeScript", () => {
assert.match(schemaOf("T", z.array(z.array(z.number()))), /type T = number\[\]\[\];/);
});

it("parenthesizes a multi-value literal element type", () => {
assert.match(schemaOf("T", z.array(z.literal(["a", "b"]))), /type T = \("a" \| "b"\)\[\];/);
});

it("parenthesizes an optional element type", () => {
assert.match(schemaOf("T", z.array(z.string().optional())), /type T = \(string \| undefined\)\[\];/);
});

it("does not parenthesize a single-value literal element type", () => {
assert.match(schemaOf("T", z.array(z.literal("a"))), /type T = "a"\[\];/);
});

});

// -----------------------------------------------------------------------
Expand Down Expand Up @@ -124,6 +136,11 @@ describe("getZodSchemaAsTypeScript", () => {
assert.match(out, /string \| number \| boolean/);
});

it("does not parenthesize optional or multi-value literal members", () => {
const out = schemaOf("T", z.union([z.string().optional(), z.literal(["a", "b"])]));
assert.match(out, /type T = string \| undefined \| "a" \| "b";/);
});

});

describe("z.discriminatedUnion()", () => {
Expand Down Expand Up @@ -153,6 +170,11 @@ describe("getZodSchemaAsTypeScript", () => {
assert.match(out, /\{[^}]*a: string[^}]*\} & \{[^}]*b: number[^}]*\}/s);
});

it("parenthesizes optional and multi-value literal operands", () => {
const out = schemaOf("T", z.intersection(z.string().optional(), z.literal(["a", "b"])));
assert.match(out, /type T = \(string \| undefined\) & \("a" \| "b"\);/);
});

});

// -----------------------------------------------------------------------
Expand All @@ -179,6 +201,21 @@ describe("getZodSchemaAsTypeScript", () => {
assert.match(out, /\[string, number\?\]/);
});

it("parenthesizes an optional rest element type", () => {
const out = schemaOf("T", z.tuple([z.string()]).rest(z.number().optional()));
assert.match(out, /\[string, \.\.\.\(number \| undefined\)\[\]\]/);
});

it("parenthesizes a multi-value literal rest element type", () => {
const out = schemaOf("T", z.tuple([z.string()]).rest(z.literal(["a", "b"])));
assert.match(out, /\[string, \.\.\.\("a" \| "b"\)\[\]\]/);
});

it("parenthesizes a multi-value literal optional element", () => {
const out = schemaOf("T", z.tuple([z.string(), z.literal(["a", "b"]).optional()]));
assert.match(out, /\[string, \("a" \| "b"\)\?\]/);
});

});

// -----------------------------------------------------------------------
Expand Down