Reputation:
I have this object:
const properties = [
{ value: "entire_place", label: "The entire place" },
{ value: "private_room", label: "A private room" },
{ value: "shared_room", label: "A shared room" },
] as const;
I need to use it with zod in order to
"entire_place" | "shared_room" | "private_room"
According to the zod documentation, i can do this:
const properties = [
{ value: "entire_place", label: "The entire place" },
{ value: "private_room", label: "A private room" },
{ value: "shared_room", label: "A shared room" },
] as const;
const VALUES = ["entire_place", "private_room", "shared_room"] as const;
const Property = z.enum(VALUES);
type Property = z.infer<typeof Property>;
However, I don't want to define my data twice, one time with a label (the label is used for ui purposes), and another without a label.
I want to define it only once using the properties
object, without the VALUES
array, and use it to create a zod object and infer the type from the zod object.
Any solutions how?
Upvotes: 27
Views: 53232
Reputation: 151
I took the example of the @Ruslan and @Souperman and create a simplified version
My propose is to wrap the z.enum function with z_enumFromArray
import { z } from 'zod'
function z_enumFromArray(array: string[]){
return z.enum([array[0], ...array.slice(1)])
}
// example of user19910212
const properties = [
{ value: "entire_place", label: "The entire place" },
{ value: "private_room", label: "A private room" },
{ value: "shared_room", label: "A shared room" },
] as const;
const propertySchema = z_enumFromArray(properties.map(prop=>prop.value))
propertySchema.parse("entire_place") // pass
propertySchema.parse("invalid_value") // throws error
// Another example using an Object as input
const status = {
active: "B0A47BD3-5CC2-49EF-BA69-9BC881A2B6C2",
inactive: "BDABF381-FA76-4578-81EC-FF3E56055A9E",
pending: "2B517D1B-1710-41A6-B946-7AE2B93C7DDE",
} as const
const statusSchema = z_enumFromArray(Object.keys(status))
statusSchema.parse("active") // pass
statusSchema.parse("invalid_value") // throws error
Upvotes: 5
Reputation: 182
Thank you, @Souperman! Your response was instrumental in helping me create a helper function to handle this situation. For those who might encounter a similar issue in the future, I've refined and documented the solution based on @souperman's guidance. Here's the helper function that extracts the values from a given enum-like object and returns them in a tuple format:
/**
* `extractValuesAsTuple` extracts the values from a given enum-like object and returns them
* in a tuple format.
*
* This helper is useful when working with libraries or utilities that expect a non-empty tuple
* of string values, particularly when those utilities cannot accept a simple string array.
*
* @template T - The type of the enum-like object. It should be a record of string keys to string values.
*
* @param {T} obj - The enum-like object from which values should be extracted.
*
* @returns {[T[keyof T], ...T[keyof T][]]} - A tuple containing all the values from the given object.
* The tuple is guaranteed to have at least one value.
*
* @example
* const Colors = { RED: 'Red', GREEN: 'Green', BLUE: 'Blue' } as const;
* const colorValues = extractValuesAsTuple(Colors); // ['Red', 'Green', 'Blue']
*
* @throws {Error} - Throws an error if the provided object is empty.
*/
function extractValuesAsTuple<T extends Record<string, any>>(
obj: T
): [T[keyof T], ...T[keyof T][]] {
const values = Object.values(obj) as T[keyof T][];
if (values.length === 0)
throw new Error('Object must have at least one value.');
// Explicitly extract the first value
const result: [T[keyof T], ...T[keyof T][]] = [values[0], ...values.slice(1)];
return result;
}
const properties = [
{ value: "entire_place", label: "The entire place" },
{ value: "private_room", label: "A private room" },
{ value: "shared_room", label: "A shared room" },
] as const;
// Extracting the 'value' properties into a tuple using the helper function
const propertyValues = extractValuesAsTuple(properties.map(p => p.value));
console.log(propertyValues); // ["entire_place", "private_room", "shared_room"]
Upvotes: 2
Reputation: 9826
In this case, I think I would infer the type for Property
from properties
directly. You can avoid repeating yourself with code like:
import { z } from "zod";
const properties = [
{ value: "entire_place", label: "The entire place" },
{ value: "private_room", label: "A private room" },
{ value: "shared_room", label: "A shared room" }
] as const;
type Property = typeof properties[number]["value"];
// z.enum expects a non-empty array so to work around that
// we pull the first value out explicitly
const VALUES: [Property, ...Property[]] = [
properties[0].value,
// And then merge in the remaining values from `properties`
...properties.slice(1).map((p) => p.value)
];
const Property = z.enum(VALUES);
Upvotes: 27