colinhacks / zod

TypeScript-first schema validation with static type inference
https://zod.dev
MIT License
33.92k stars 1.18k forks source link

Use arrays to create unions #3651

Open mildfuzz opened 3 months ago

mildfuzz commented 3 months ago

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

scotttrinh commented 3 months ago

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).

dpolugic commented 3 months ago

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>
mildfuzz commented 3 months ago

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 {

}

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 // 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

— 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: @.***>