zod icon indicating copy to clipboard operation
zod copied to clipboard

Use arrays to create unions

Open mildfuzz opened this issue 1 year ago • 4 comments

Trying to something like this, just to reduce noise of lots of potential literal values:

const unionSchema = z.union(([0,2,5,7]).map(i =>z.literal(i)))
type UnionType = z.infer<typeof unionSchema>

but right now, UnionType resolves to any

mildfuzz avatar Jul 18 '24 13:07 mildfuzz

Yeah, you cannot do dynamic things (like call map) to schemas because the types won't match up. This is just a little bit of safety that the TypeScript compiler provides since at runtime the compiler doesn't keep track of how you're updating your as const array.

I definitely get wanting to improve the ergonomics here to avoid having to wrap each value in a literal but you have a few other options:

  1. Use an alias for z.literal to make it a little easier:
    const l = z.literal;
    
  2. Use a z.custom schema and provide the type explicitly:
    const unionLiterals = [0, 2, 5, 7] as const;
    const unionSchema = z.custom<(typeof unionLiterals)[number]>((v) =>
      unionLiterals.includes(v),
    );
    type UnionType = z.infer<typeof unionSchema>;
    

If it were me, I'd either just type out the z.literal(val) syntax and just chalk it up to typing practice 😅 or use the custom schema if it's a really large array that I was copying from somewhere else (like an array of all locales, or country names, or something like that).

scotttrinh avatar Jul 18 '24 14:07 scotttrinh

While TypeScript doesn't support preserving tuple types when using Array.prototype.map (see https://github.com/microsoft/TypeScript/issues/29841), for this case we could define our own mapping function. I think something this could work for your initial question:

function zodLiteralUnion<T extends readonly [z.Primitive, z.Primitive, ...z.Primitive[]]>(
  primitives: [...T]
) {
  const literals = primitives.map(x => z.literal(x)) as {
    [Index in keyof T]: z.ZodLiteral<T[Index]>
  }

  return z.union(literals)
}

// const A: z.ZodUnion<[z.ZodLiteral<1>, z.ZodLiteral<2>, z.ZodLiteral<3>]>
const A = z.union([z.literal(1), z.literal(2), z.literal(3)])
// type A = 2 | 1 | 3
type A = z.infer<typeof A>

// const B: z.ZodUnion<[z.ZodLiteral<1>, z.ZodLiteral<2>, z.ZodLiteral<3>]>
const B = zodLiteralUnion([1, 2, 3])
// type B = 2 | 1 | 3
type B = z.infer<typeof B>

dpolugic avatar Jul 19 '24 06:07 dpolugic

Ooh, that's nice

On Fri, 19 Jul 2024, 07:26 Damjan Polugic, @.***> wrote:

While TypeScript doesn't support preserving tuple types when using Array.prototype.map (see microsoft/TypeScript#29841 https://github.com/microsoft/TypeScript/issues/29841), for this case we could define our own mapping function. I think something this could work for your initial question:

function zodLiteralUnion<T extends readonly [z.Primitive, z.Primitive, ...z.Primitive[]]>( primitives: [...T]) { const literals = primitives.map(x => z.literal(x)) as { [Index in keyof T]: z.ZodLiteral<T[Index]> }

return z.union(literals)} // const A: z.ZodUnion<[z.ZodLiteral<1>, z.ZodLiteral<2>, z.ZodLiteral<3>]>const A = z.union([z.literal(1), z.literal(2), z.literal(3)])// type A = 2 | 1 | 3type A = z.infer<typeof A> // const B: z.ZodUnion<[z.ZodLiteral<1>, z.ZodLiteral<2>, z.ZodLiteral<3>]>const B = zodLiteralUnion([1, 2, 3])// type B = 2 | 1 | 3type B = z.infer<typeof B>

— Reply to this email directly, view it on GitHub https://github.com/colinhacks/zod/issues/3651#issuecomment-2238385279, or unsubscribe https://github.com/notifications/unsubscribe-auth/AABQAYOLSLYIGUHECNFQ6OLZNCWSJAVCNFSM6AAAAABLCW4GRWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDEMZYGM4DKMRXHE . You are receiving this because you authored the thread.Message ID: @.***>

mildfuzz avatar Jul 19 '24 11:07 mildfuzz

Hi, @mildfuzz. I'm Dosu, and I'm helping the Zod team manage their backlog. I'm marking this issue as stale.

Issue Summary:

  • You raised an issue about using arrays to create union types with z.literal, which resulted in unexpected type inference to any.
  • @scotttrinh explained the limitations of TypeScript with dynamic operations like map on schemas and suggested alternatives.
  • @dpolugic proposed a custom mapping function to preserve tuple types, which you appreciated.
  • The discussion provided practical solutions, such as using a custom schema or alias for z.literal, addressing the issue.

Next Steps:

  • Please let me know if this issue is still relevant to the latest version of the Zod repository. If so, you can keep the discussion open by commenting on the issue.
  • Otherwise, the issue will be automatically closed in 7 days.

Thank you for your understanding and contribution!

dosubot[bot] avatar Jun 18 '25 16:06 dosubot[bot]