If you encounter a similar situation, one approach is to utilize z.union
along with z.literal
to make the code more declarative:
const schema = z.object({
prop: z.union([z.literal(5), z.literal(10), z.literal(15)]),
});
This method also offers the advantage of defining the exact type 5 | 10 | 15
as the output rather than just number
.
However, writing this out can become cumbersome, so it might be beneficial to create a helper function if you find yourself repeating this frequently:
// While this signature may seem excessive, it encompasses all types that
// z.literal can work with.
function oneOf<T extends string | number | boolean | bigint | null | undefined>(
t: readonly [T, T, ...T[]],
) {
// Since a union necessitates at least 2 elements for proper typing,
// it must be instantiated in this manner.
return z.union([
z.literal(t[0]),
z.literal(t[1]),
// We cannot use pointfree syntax here since z.literal accepts an optional second parameter
...t.slice(2).map(v => z.literal(v)),
])
}
// Using this will result in a schema equivalent to the initial example
const schema2 = z.object({
// Without 'as const', this will still pass type checking but with looser
// types compared to the first scenario.
prop: oneOf([5,10,15] as const),
});