Dev Build / cleanup (push) Canceled after 0s
Dev Build / typecheck (push) Canceled after 0s
Dev Build / build (linux-arm64, ubuntu-24.04-arm, linux-arm64) (push) Canceled after 0s
Dev Build / build (linux-x64, ubuntu-latest, linux-x64) (push) Canceled after 0s
Dev Build / build (win-x64, windows-latest, win32-x64) (push) Canceled after 0s
Dev Build / dispatch-dev-docker (push) Canceled after 0s
236 lines
11 KiB
Plaintext
236 lines
11 KiB
Plaintext
import { expect, expectTypeOf, test } from "vitest";
|
|
import { z } from "zod/v4";
|
|
|
|
/* ------------------------------------------------------------------ *
|
|
* TYPE-LEVEL — the dangerous half. Defaults must stay OPTIONAL in
|
|
* z.input and REQUIRED in z.output, exactly as before the ladder.
|
|
* ------------------------------------------------------------------ */
|
|
test("ladder: z.input / z.output unchanged by the extra rung", () => {
|
|
const objDefault = z.object({ a: z.string().default("D"), b: z.number() });
|
|
expectTypeOf<z.input<typeof objDefault>>().toEqualTypeOf<{ a?: string | undefined; b: number }>();
|
|
expectTypeOf<z.output<typeof objDefault>>().toEqualTypeOf<{ a: string; b: number }>();
|
|
|
|
const objPrefault = z.object({ a: z.string().prefault("P"), b: z.number() });
|
|
expectTypeOf<z.input<typeof objPrefault>>().toEqualTypeOf<{ a?: string | undefined; b: number }>();
|
|
expectTypeOf<z.output<typeof objPrefault>>().toEqualTypeOf<{ a: string; b: number }>();
|
|
|
|
const objOptional = z.object({ a: z.string().optional() });
|
|
expectTypeOf<z.input<typeof objOptional>>().toEqualTypeOf<{ a?: string | undefined }>();
|
|
|
|
const objTransform = z.object({
|
|
a: z
|
|
.string()
|
|
.default("")
|
|
.transform((v) => v.length),
|
|
});
|
|
expectTypeOf<z.input<typeof objTransform>>().toEqualTypeOf<{ a?: string | undefined }>();
|
|
expectTypeOf<z.output<typeof objTransform>>().toEqualTypeOf<{ a: number }>();
|
|
|
|
const standalone = z.string().default("D");
|
|
expectTypeOf<z.input<typeof standalone>>().toEqualTypeOf<string | undefined>();
|
|
expectTypeOf<z.output<typeof standalone>>().toEqualTypeOf<string>();
|
|
|
|
// a trailing defaulted tuple slot stays optional in the input tuple
|
|
const tup = z.tuple([z.string(), z.number().default(1)]);
|
|
expectTypeOf<z.input<typeof tup>>().toEqualTypeOf<[string, (number | undefined)?]>();
|
|
|
|
const partialed = objDefault.partial();
|
|
expectTypeOf<z.input<typeof partialed>>().toEqualTypeOf<{ a?: string | undefined; b?: number | undefined }>();
|
|
});
|
|
|
|
/* ------------------------------------------------------------------ *
|
|
* RUNTIME
|
|
* ------------------------------------------------------------------ */
|
|
test("ladder: the #6321 regression is fixed", () => {
|
|
const arr = z
|
|
.string()
|
|
.default("")
|
|
.transform((v) => (v ? v.split(",") : []));
|
|
expect(z.object({ a: arr }).partial().parse({})).toEqual({ a: [] });
|
|
expect(z.object({ a: arr }).partial().parse({ a: undefined })).toEqual({ a: [] });
|
|
expect(z.object({ a: arr }).partial().parse({ a: "x,y" })).toEqual({ a: ["x", "y"] });
|
|
expect(arr.optional().parse(undefined)).toEqual([]);
|
|
expect(
|
|
z
|
|
.string()
|
|
.prefault("hi")
|
|
.transform((v) => v.length)
|
|
.optional()
|
|
.parse(undefined)
|
|
).toBe(2);
|
|
});
|
|
|
|
test("ladder: key-level admissibility across every rung", () => {
|
|
expect(z.object({ a: z.string().default("D") }).parse({})).toEqual({ a: "D" });
|
|
expect(z.object({ a: z.string().prefault("P") }).parse({})).toEqual({ a: "P" });
|
|
expect(z.object({ a: z.string().optional() }).parse({})).toEqual({});
|
|
expect(z.object({ a: z.transform(() => "T") }).parse({})).toEqual({ a: "T" });
|
|
expect(z.object({ a: z.string().catch("C") }).parse({})).toEqual({ a: "C" });
|
|
expect(z.object({ a: z.string() }).safeParse({}).success).toBe(false);
|
|
});
|
|
|
|
test("ladder: exactOptional keeps its distinct meaning", () => {
|
|
expect(z.object({ a: z.string().exactOptional() }).parse({})).toEqual({});
|
|
expect(z.object({ a: z.string().exactOptional() }).safeParse({ a: undefined }).success).toBe(false);
|
|
});
|
|
|
|
// An absent key never materializes a value from a schema that only claims the middle rung, however that schema answers `undefined`. The top rung still substitutes, and an explicitly present `undefined` still runs the inner.
|
|
test.each([true, false])("ladder: an absent key stays absent on the middle rung (jitless=%s)", (jitless) => {
|
|
const opts = { jitless };
|
|
expect(z.object({ a: z.coerce.string().exactOptional() }).parse({}, opts)).toStrictEqual({});
|
|
expect(z.object({ a: z.string().catch("C").exactOptional() }).parse({}, opts)).toStrictEqual({});
|
|
expect(z.object({ a: z.preprocess((v) => v ?? "X", z.string()).exactOptional() }).parse({}, opts)).toStrictEqual({});
|
|
expect(z.object({ a: z.union([z.coerce.string(), z.string().optional()]) }).parse({}, opts)).toStrictEqual({});
|
|
|
|
expect(z.object({ a: z.string().default("D").exactOptional() }).parse({}, opts)).toStrictEqual({ a: "D" });
|
|
expect(z.object({ a: z.string().prefault("P").exactOptional() }).parse({}, opts)).toStrictEqual({ a: "P" });
|
|
expect(z.object({ a: z.coerce.string().exactOptional() }).parse({ a: undefined }, opts)).toStrictEqual({
|
|
a: "undefined",
|
|
});
|
|
expect(z.object({ a: z.coerce.string() }).parse({ a: undefined }, opts)).toStrictEqual({ a: "undefined" });
|
|
});
|
|
|
|
test("ladder: an absent tuple slot on the middle rung truncates the tail", () => {
|
|
expect(z.tuple([z.string(), z.coerce.string().exactOptional()]).parse(["x"])).toStrictEqual(["x"]);
|
|
expect(z.tuple([z.string(), z.string().catch("C").exactOptional()]).parse(["x"])).toStrictEqual(["x"]);
|
|
expect(z.tuple([z.string(), z.string().default("D").exactOptional()]).parse(["x"])).toStrictEqual(["x", "D"]);
|
|
});
|
|
|
|
// z.compile() assembles its own output, so the gate has to be mirrored there or compile mode silently keeps the invented value.
|
|
test("ladder: compile mode agrees with the interpreted and JIT paths on absent middle-rung slots", () => {
|
|
const objects = [
|
|
[z.object({ a: z.coerce.string().exactOptional() }), {}],
|
|
[z.object({ a: z.string().catch("C").exactOptional() }), {}],
|
|
[z.object({ a: z.string().default("D").exactOptional() }), { a: "D" }],
|
|
[z.object({ a: z.string().catch("C") }), { a: "C" }],
|
|
] as const;
|
|
for (const [schema, expected] of objects) {
|
|
expect(z.compile(schema).parse({})).toStrictEqual(expected);
|
|
expect(schema.parse({}, { jitless: true })).toStrictEqual(expected);
|
|
expect(schema.parse({})).toStrictEqual(expected);
|
|
}
|
|
|
|
const tuples = [
|
|
[z.tuple([z.string(), z.coerce.string().exactOptional()]), ["x"]],
|
|
[z.tuple([z.string(), z.string().default("D").exactOptional()]), ["x", "D"]],
|
|
] as const;
|
|
for (const [schema, expected] of tuples) {
|
|
expect(z.compile(schema).parse(["x"])).toStrictEqual(expected);
|
|
expect(schema.parse(["x"], { jitless: true })).toStrictEqual(expected);
|
|
}
|
|
});
|
|
|
|
test("ladder: tuple minimum length respects every rung", () => {
|
|
expect(z.tuple([z.string().default("D")]).parse([])).toEqual(["D"]);
|
|
expect(z.tuple([z.string().prefault("P")]).parse([])).toEqual(["P"]);
|
|
expect(z.tuple([z.string().optional()]).parse([])).toEqual([]);
|
|
expect(z.tuple([z.string()]).safeParse([]).success).toBe(false);
|
|
expect(z.tuple([z.string(), z.number().default(1)]).parse(["x"])).toEqual(["x", 1]);
|
|
});
|
|
|
|
test("ladder: the #5941 / #5939 clobber invariants still hold", () => {
|
|
expect(
|
|
z
|
|
.preprocess((v) => v ?? "X", z.string())
|
|
.optional()
|
|
.parse(undefined)
|
|
).toBeUndefined();
|
|
expect(z.string().catch("X").optional().parse(undefined)).toBeUndefined();
|
|
expect(
|
|
z
|
|
.string()
|
|
.catch("X")
|
|
.transform((s) => `${s}!`)
|
|
.optional()
|
|
.parse(undefined)
|
|
).toBeUndefined();
|
|
expect(
|
|
z
|
|
.transform(() => "T")
|
|
.optional()
|
|
.parse(undefined)
|
|
).toBeUndefined();
|
|
expect(
|
|
z
|
|
.union([z.preprocess((v) => v ?? "X", z.string()), z.number()])
|
|
.optional()
|
|
.parse(undefined)
|
|
).toBeUndefined();
|
|
expect(z.object({ a: z.preprocess((v) => v ?? "X", z.string()) }).parse({})).toEqual({ a: "X" });
|
|
});
|
|
|
|
test("ladder: the top rung survives every wrapper order", () => {
|
|
expect(z.string().default("D").optional().parse(undefined)).toBe("D");
|
|
expect(z.string().default("D").optional().optional().parse(undefined)).toBe("D");
|
|
expect(z.string().default("D").nullable().optional().parse(undefined)).toBe("D");
|
|
expect(z.string().default("D").readonly().optional().parse(undefined)).toBe("D");
|
|
expect(z.string().default("D").catch("C").optional().parse(undefined)).toBe("D");
|
|
expect(z.string().default("D").pipe(z.string()).optional().parse(undefined)).toBe("D");
|
|
expect(
|
|
z
|
|
.union([z.string().default("D"), z.number()])
|
|
.optional()
|
|
.parse(undefined)
|
|
).toBe("D");
|
|
expect(
|
|
z
|
|
.lazy(() => z.string().default("D"))
|
|
.optional()
|
|
.parse(undefined)
|
|
).toBe("D");
|
|
});
|
|
|
|
test("ladder: record and catchall paths that read optin", () => {
|
|
expect(z.record(z.string(), z.string().default("D")).parse({ k: undefined })).toEqual({ k: "D" });
|
|
expect(z.object({}).catchall(z.string().default("D")).parse({ x: undefined })).toEqual({ x: "D" });
|
|
});
|
|
|
|
test("ladder: async parity", async () => {
|
|
const a = z
|
|
.string()
|
|
.default("")
|
|
.transform(async (v) => (v ? v.split(",") : []));
|
|
expect((await a.optional().safeParseAsync(undefined)).data).toEqual([]);
|
|
expect((await z.object({ a }).partial().safeParseAsync({})).data).toEqual({ a: [] });
|
|
expect(
|
|
(
|
|
await z
|
|
.preprocess(async (v) => v ?? "X", z.string())
|
|
.optional()
|
|
.safeParseAsync(undefined)
|
|
).data
|
|
).toBeUndefined();
|
|
});
|
|
|
|
test("ladder: the rung values themselves", () => {
|
|
expect(z.string()._zod.optin).toBeUndefined();
|
|
expect(z.string().optional()._zod.optin).toBe("optional");
|
|
expect(z.transform(() => 1)._zod.optin).toBe("optional");
|
|
expect(z.string().catch("C")._zod.optin).toBe("optional");
|
|
expect(z.string().default("D")._zod.optin).toBe("defaulted");
|
|
expect(z.string().prefault("P")._zod.optin).toBe("defaulted");
|
|
expect(
|
|
z
|
|
.string()
|
|
.default("D")
|
|
.transform((v) => v)._zod.optin
|
|
).toBe("defaulted");
|
|
expect(z.string().default("D").optional()._zod.optin).toBe("defaulted");
|
|
expect(z.union([z.string().default("D"), z.number()])._zod.optin).toBe("defaulted");
|
|
expect(z.union([z.string().optional(), z.number()])._zod.optin).toBe("optional");
|
|
});
|
|
|
|
test("exactOptional overrides the values and pattern optional installs", () => {
|
|
expect(z.exactOptional(z.enum(["a", "b"]))._zod.values).toEqual(new Set(["a", "b"]));
|
|
// a format carries its pattern on its def; a chained `.regex()` no longer folds into the inner `_zod.pattern`, the template literal derives that itself
|
|
expect(z.exactOptional(z.stringFormat("abc", /^abc$/))._zod.pattern).toEqual(/^abc$/);
|
|
expect(z.templateLiteral(["a", z.exactOptional(z.literal("b"))]).safeParse("a").success).toEqual(false);
|
|
});
|
|
|
|
// The override is installed once, on a prototype every instance of the type shares, so it has to hold for instances built after the first.
|
|
test("exactOptional's override holds for later instances", () => {
|
|
z.exactOptional(z.enum(["a", "b"]));
|
|
expect(z.exactOptional(z.enum(["c", "d"]))._zod.values).toEqual(new Set(["c", "d"]));
|
|
expect(z.exactOptional(z.stringFormat("xyz", /^xyz$/))._zod.pattern).toEqual(/^xyz$/);
|
|
});
|