import { expect, expectTypeOf, test } from "vitest"; import { z } from "zod/v4"; test("recursion with z.lazy", () => { const data = { name: "I", subcategories: [ { name: "A", subcategories: [ { name: "1", subcategories: [ { name: "a", subcategories: [], }, ], }, ], }, ], }; const Category = z.object({ name: z.string(), get subcategories() { return z.array(Category).optional().nullable(); }, }); type Category = z.infer; interface _Category { name: string; subcategories?: _Category[] | undefined | null; } expectTypeOf().toEqualTypeOf<_Category>(); Category.parse(data); }); test("recursion involving union type", () => { const data = { value: 1, next: { value: 2, next: { value: 3, next: { value: 4, next: null, }, }, }, }; const LL = z.object({ value: z.number(), get next() { return LL.nullable(); }, }); type LL = z.infer; type _LL = { value: number; next: _LL | null; }; expectTypeOf().toEqualTypeOf<_LL>(); LL.parse(data); }); test("mutual recursion - native", () => { const Alazy = z.object({ val: z.number(), get b() { return Blazy; }, }); const Blazy = z.object({ val: z.number(), get a() { return Alazy.optional(); }, }); const testData = { val: 1, b: { val: 5, a: { val: 3, b: { val: 4, a: { val: 2, b: { val: 1, }, }, }, }, }, }; type Alazy = z.infer; type Blazy = z.infer; interface _Alazy { val: number; b: _Blazy; } interface _Blazy { val: number; a?: _Alazy | undefined; } expectTypeOf().toEqualTypeOf<_Alazy>(); expectTypeOf().toEqualTypeOf<_Blazy>(); Alazy.parse(testData); Blazy.parse(testData.b); expect(() => Alazy.parse({ val: "asdf" })).toThrow(); }); test("pick and omit with getter", () => { const Category = z.strictObject({ name: z.string(), get subcategories() { return z.array(Category); }, }); type Category = z.infer; interface _Category { name: string; subcategories: _Category[]; } expectTypeOf().toEqualTypeOf<_Category>(); const PickedCategory = Category.pick({ name: true }); const OmittedCategory = Category.omit({ subcategories: true }); const picked = { name: "test" }; const omitted = { name: "test" }; PickedCategory.parse(picked); OmittedCategory.parse(omitted); expect(() => PickedCategory.parse({ name: "test", subcategories: [] })).toThrow(); expect(() => OmittedCategory.parse({ name: "test", subcategories: [] })).toThrow(); }); test("deferred self-recursion", () => { const Feature = z.object({ title: z.string(), get features(): z.ZodOptional> { return z.optional(z.array(Feature)); //.optional(); }, }); // type Feature = z.infer; const Output = z.object({ id: z.int(), //.nonnegative(), name: z.string(), get features(): z.ZodArray { return Feature.array(); }, }); type Output = z.output; type _Feature = { title: string; features?: _Feature[] | undefined; }; type _Output = { id: number; name: string; features: _Feature[]; }; // expectTypeOf().toEqualTypeOf<_Feature>(); expectTypeOf().toEqualTypeOf<_Output>(); }); test("deferred mutual recursion", () => { const Slot = z.object({ slotCode: z.string(), get blocks() { return z.array(Block); }, }); type Slot = z.infer; const Block = z.object({ blockCode: z.string(), get slots() { return z.array(Slot).optional(); }, }); type Block = z.infer; const Page = z.object({ slots: z.array(Slot), }); type Page = z.infer; type _Slot = { slotCode: string; blocks: _Block[]; }; type _Block = { blockCode: string; slots?: _Slot[] | undefined; }; type _Page = { slots: _Slot[]; }; expectTypeOf().toEqualTypeOf<_Slot>(); expectTypeOf().toEqualTypeOf<_Block>(); expectTypeOf().toEqualTypeOf<_Page>(); }); test("mutual recursion with meta", () => { const A = z .object({ name: z.string(), get b() { return B; }, }) .readonly() .meta({ id: "A" }) .optional(); const B = z .object({ name: z.string(), get a() { return A; }, }) .readonly() .meta({ id: "B" }); type A = z.infer; type B = z.infer; type _A = | Readonly<{ name: string; b: _B; }> | undefined; // | undefined; type _B = Readonly<{ name: string; a?: _A; }>; expectTypeOf().toEqualTypeOf<_A>(); expectTypeOf().toEqualTypeOf<_B>(); }); test("recursion compatibility", () => { // array const A = z.object({ get array() { return A.array(); }, get optional() { return A.optional(); }, get nullable() { return A.nullable(); }, get nonoptional() { return A.nonoptional(); }, get readonly() { return A.readonly(); }, get describe() { return A.describe("A recursive type"); }, get meta() { return A.meta({ description: "A recursive type" }); }, get pipe() { return A.pipe(z.any()); }, get strict() { return A.strict(); }, get tuple() { return z.tuple([A, A]); }, get object() { return z .object({ subcategories: A, }) .strict() .loose(); }, get union() { return z.union([A, A]); }, get intersection() { return z.intersection(A, A); }, get record() { return z.record(z.string(), A); }, get map() { return z.map(z.string(), A); }, get set() { return z.set(A); }, get lazy() { return z.lazy(() => A); }, get promise() { return z.promise(A); }, }); }); // biome-ignore lint: sadf export type RecursiveA = z.ZodUnion< [ z.ZodObject<{ a: z.ZodDefault; b: z.ZodPrefault; c: z.ZodNonOptional; d: z.ZodOptional; e: z.ZodNullable; g: z.ZodReadonly; h: z.ZodPipe; i: z.ZodArray; j: z.ZodSet; k: z.ZodMap; l: z.ZodRecord; m: z.ZodUnion<[RecursiveA, RecursiveA]>; n: z.ZodIntersection; o: z.ZodLazy; p: z.ZodPromise; q: z.ZodCatch; r: z.ZodSuccess; s: z.ZodTransform; t: z.ZodTuple<[RecursiveA, RecursiveA]>; u: z.ZodObject<{ a: RecursiveA; }>; }>, ] >;