colinhacks / zod

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

z.union allow ZodType array spread as argument #3383

Open FlynnHillier opened 7 months ago

FlynnHillier commented 7 months ago

I am attempting to use a dynamically generated array of strings to define a z.union() that validates that a string is included within said dynamic array.

To do this I am mapping each element of my dynamic array of strings to z.literal(), and then spreading this mapped array of literals to the union type.

import { z } from "zod"
const TILES = ["a1","a2","a3","b1","b2","b3"]

// ERROR:
// Argument of type 'ZodLiteral<string>[]' is not assignable to parameter of type 'readonly [ZodTypeAny, ZodTypeAny, ...ZodTypeAny[]]'.
// Source provides no match for required element at position 0 in target.ts(2345)
const myUnion = z.union([
    ...TILES.map(tile => z.literal(tile))
])

This implementation results in typescript kicking up a fuss, as it is not sure that union has been provided any values. This is because zod's union type is hardcoded to have atleast 2 ZodTypes provided to it.

declare const unionType: <T extends readonly [ZodTypeAny, ZodTypeAny, ...ZodTypeAny[]]>(types: T, params?: RawCreateParams) => ZodUnion<T>;

This means to prevent typescript from complaining, 2 literals must be hardcoded before we spread our array of literals into the union.

const myUnion = z.union([
    z.literal("a1"),
    z.literal("a2"),
    ...TILES.map(tile => z.literal(tile))
])

This is evidently not ideal as we may not have two values which we always wish to be included in our union type.

Is there any way z.union() could be re-typed / changed in some way to better support spreading of ZodType arrays?

extradosages commented 7 months ago

This is a good idea; one-element and even zero-element unions have reasonable conventional semantics, and can probably be easily implemented in the type system with conditionals!

colinhacks commented 6 months ago

My goal was for ZodUnion to have a strongly typed tuple containing the element schemas. The current type signature is designed to prevent things like your TILES.map() call, because then you can't do something like this:

myUnion.options[2]; 
// should be ZodLiteral<"a3">

I'm looking into loosening this restriction in the next major version. This is a recurring issue for people, and it doesn't impact the inferred type so it's probably not worth the trouble.