Enum From Object Literal Keys #839
-
As discussed in other issues in this repo, it is not trivial to create an enum from an object literal keys, or even from an array of strings: ( Any objection to the following solution? import { z } from 'zod';
import { keys } from 'fp-ts/lib/Record';
export function ObjectKeysEnum<K extends string>(obj: Record<K, any>) {
return z.enum([keys(obj)[0], ...keys(obj)]);
} |
Beta Was this translation helpful? Give feedback.
Replies: 5 comments 11 replies
-
I'm not exactly sure what you are asking, but it seems that your solution works ok. However, the types aren't intuitive in my opinion, which is not your fault. Unfortunately it's a problem with typescript. If I were doing this, I would expect that see here for more info: Since // I'm not familiar with fp-ts, so I made my own keys function, I'm pretty sure it's close enough to what you were using.
const keys = <K extends string> ( r: Record<K, any> ): K[] => Object.keys( r ) as K[]
export function ObjectKeysEnum<K extends string> ( obj: Record<K, any> ) {
return z.enum( [ keys( obj )[ 0 ], ...keys( obj ) ] )
}
const obj = { bar: 'bar', baz: 'baz' } as const
type ObjKeys = keyof typeof obj
//-> ObjKeys = "bar" | "baz"
const keysEnum = ObjectKeysEnum( obj )
// -> keysEnum: z.ZodEnum<["bar" | "baz", ...("bar" | "baz")[]]>
keysEnum.safeParse( 'bar' ).success // true
keysEnum.safeParse( 'baz' ).success // true
keysEnum.safeParse( 'something else' ).success // false I hope this helps, please let me know if you have any questions. Or if I didn't answer your question, please clarify what you are asking. |
Beta Was this translation helpful? Give feedback.
-
Thanks! I think we understand each other. I haven't deeply read the Stack Overflow explanation, but looks like this "ObjectKeysEnum" util does what it does 100% fine, and is very useful in many situations - so I would suggest adding something like that to Zod's core. |
Beta Was this translation helpful? Give feedback.
-
I think I understand the problem better now. Thanks. Here's how I would solve it: function zodEnumFromObjKeys<K extends string> ( obj: Record<K, any> ): z.ZodEnum<[ K, ...K[] ]> {
const [ firstKey, ...otherKeys ] = Object.keys( obj ) as K[]
return z.enum( [ firstKey, ...otherKeys ] )
} type Post = {
createdAt: number
comments: string[]
}
const ordering = {
new: ( a: Post, b: Post ) => b.createdAt - a.createdAt,
old: ( a: Post, b: Post ) => a.createdAt - b.createdAt,
popular: ( a: Post, b: Post ) => b.comments.length - a.comments.length,
} as const
// } satisfies Record<string, (a: Post, b: Post) => number>
// you could use the new `satisfies` keyword here,
// but I'm using deno so I don't have that until the next release in a week or so. :(
const orderingSchema = zodEnumFromObjKeys( ordering )
type Ordering = z.infer<typeof orderingSchema>
// type Ordering = 'new' | 'old' | 'popular'
const sortParam = new URLSearchParams( 'sort=new' ).get( 'sort' )
const posts: Post[] = [
{
createdAt: 1,
comments: [ 'a', 'b' ],
},
{
createdAt: 2,
comments: [ 'a', 'b', 'c' ],
},
]
const sortedPosts = posts.slice().sort(
ordering[ orderingSchema.parse( sortParam ) ]
)
console.log( sortParam )
// 'new'
console.log( posts )
// [
// { createdAt: 1, comments: [ "a", "b" ] },
// { createdAt: 2, comments: [ "a", "b", "c" ] }
// ]
console.log( sortedPosts )
// [
// { createdAt: 2, comments: [ "a", "b", "c" ] },
// { createdAt: 1, comments: [ "a", "b" ] }
// ] |
Beta Was this translation helpful? Give feedback.
-
I was just having this issue. My simple fix was: const [firstKey, ...otherKeys] = Object.keys(MY_OBJECT)
z.enum([firstKey, ...otherKeys])
// ...if that still throws an error, also try:
z.enum([firstKey!, ...otherKeys]) |
Beta Was this translation helpful? Give feedback.
-
the simplest solution I could get by combining answers of @feychenie and @rstacruz: Solution that provides autocompletion and validates input/** Converts a plain object's keys into ZodEnum with type safety and autocompletion */
function getZodEnumFromObjectKeys<TI extends Record<string, any>, R extends string = TI extends Record<infer R, any> ? R : never>(input: TI): z.ZodEnum<[R, ...R[]]> {
const [firstKey, ...otherKeys] = Object.keys(input) as [R, ...R[]];
return z.enum([firstKey, ...otherKeys]);
}
// now let's define a plain object with string keys
const countries = {
"NL": 1,
"BE": 2,
};
// and convert it into ZodEnum that supports autocompletion
const zCountries = getZodEnumFromObjectKeys(countries);
// ^ const zCountries: z.ZodEnum<["NL" | "BE", ...("NL" | "BE")[]]>
type Country = z.infer<typeof zCountries>;
// ^ type Country = "NL" | "BE"
const country = zCountries.parse(YOUR_INPUT);
// ^ const country: "NL" | "BE" |
Beta Was this translation helpful? Give feedback.
@sbking @danielyogel
I think I understand the problem better now. Thanks.
Here's how I would solve it: