zod icon indicating copy to clipboard operation
zod copied to clipboard

Is there a way to combine `discriminatedUnion` with `lazy`?

Open LuxXx opened this issue 3 years ago • 4 comments

Hello,

I stumbled on this problem.

When using lazy (recursive types) I cannot use discriminatedUnion anymore.

import z from "zod";

type SomeOtherObject = {
  type: "some-other-type";
  randomProperty: string;
};

const someOtherObjectWithADiscriminatingType = z.object({
  type: z.literal("some-other-type"),
  randomProperty: z.string()
});

type RecursiveElement = {
  type: "recursive-element";
  children: MyUnion[];
};

const recursiveElement: z.ZodType<RecursiveElement> = z.lazy(() =>
  z.object({
    type: z.literal("recursive-element"),
    children: myUnion.array()
  })
);

type MyUnion = RecursiveElement | SomeOtherObject;

What works is

const myUnion = z.union([
  recursiveElement,
  someOtherObjectWithADiscriminatingType
]);

But I would love to use discriminatedUnion here.

const myUnion = z.discriminatedUnion("type",[
  recursiveElement,
  someOtherObjectWithADiscriminatingType
]);

Any way I can type lazy object recursiveElement somehow that it does not throw an error?

Here is this problem in CodeSandBox: https://codesandbox.io/s/lucid-wilson-k5x1pu?file=/src/App.tsx:682-792

LuxXx avatar Oct 18 '22 16:10 LuxXx

One option here is to make myUnion lazy instead of making the union elements lazy.

type MyUnion =
  | z.infer<typeof someOtherObjectWithADiscriminatingType>
  | RecursiveElement; // Can't use z.infer here, use explicit type

const myUnion: z.ZodType<MyUnion> = z.lazy(() =>
  z.discriminatedUnion("type", [
    recursiveElement,
    someOtherObjectWithADiscriminatingType
  ]),
);

const someOtherObjectWithADiscriminatingType = z.object({
  type: z.literal("some-other-type"),
  randomProperty: z.string(),
});

const recursiveElement = z.object({
  type: z.literal("recursive-element"),
  children: myUnion.array(),
}); // With TS 4.9, could add `satisfies z.ZodType<RecursiveElement>` here
type RecursiveElement = {
  type: "recursive-element";
  children: MyUnion[]
}

(Edited to fix code)

In my experience this works nicely, because you only ever have one lazy schema (the union), even if you add additional recursive union elements.

itsgiacoliketaco avatar Oct 18 '22 18:10 itsgiacoliketaco

This does not work for me. I cannot define recursiveElement without lazy.

LuxXx avatar Oct 19 '22 02:10 LuxXx

I'm not sure if you have some different limitation that you're not describing, but I did realize my above suggestion was mistaken. You must additionally write a manual type definition for the RecursiveElement type:

type MyUnion =
  | z.infer<typeof someOtherObjectWithADiscriminatingType>
  | RecursiveElement; // Can't use z.infer here, use explicit type

const myUnion: z.ZodType<MyUnion> = z.lazy(() =>
  z.discriminatedUnion("type", [
    recursiveElement,
    someOtherObjectWithADiscriminatingType
  ]),
);

const someOtherObjectWithADiscriminatingType = z.object({
  type: z.literal("some-other-type"),
  randomProperty: z.string(),
});

const recursiveElement = z.object({
  type: z.literal("recursive-element"),
  children: myUnion.array(),
}); // With TS 4.9, could add `satisfies z.ZodType<RecursiveElement>` here
type RecursiveElement = {
  type: "recursive-element";
  children: MyUnion[]
}

This definitely works (I've used this pattern myself). Do you have some other reason why you can't define recursiveElement without z.lazy()?

itsgiacoliketaco avatar Oct 20 '22 00:10 itsgiacoliketaco

I've got this openned PR https://github.com/colinhacks/zod/pull/1290 fixing this issue and waiting for feed back or merge.

roblabat avatar Oct 28 '22 14:10 roblabat

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Jan 26 '23 21:01 stale[bot]

See also this Stack Overflow answer that provides a different solution.

feynmanix avatar Mar 23 '24 21:03 feynmanix