Open OnkelTem opened 1 year ago
Thanks to folks from the TypeScript Discord channel, I've finally come up with this solution.
import { z } from "zod";
// API: we have some API which returns themes by color ids
enum color {
red = "red",
green = "green",
};
type ColorId = keyof typeof color
type ThemeRequest = {
colorId: ColorId;
};
declare function getThemeApi(req: ThemeRequest): any;
// UTILTIY TYPES, to convert TS union to TS tuple
type ToIntersection<T> = (T extends T ? (_: T) => 0 : never) extends (_: infer U) => 0
? U
: never;
type Last<T> = ToIntersection<T extends unknown ? () => T : never> extends () => infer U
? U
: never;
type Keys<T> = [T] extends [never] ? [] : [...Keys<Exclude<T, Last<T>>>, Last<T>];
// UTILITY TYPE, that takes object type and retuns a union of ZodLiteral's
type MakeValues<T> = { [K in keyof T]: z.ZodLiteral<T[K]> }[keyof T]
type _V = MakeValues<typeof color>
//^?
// UTILITY FUNCTION, that takes object and returns an array z.literal() but asserts its
// type to be a tuple of specific values from the object keys.
function makeValues<T extends Record<string, unknown>>(options: T) {
return Object.keys(options).map((k) => z.literal(k)) as Keys<MakeValues<T>>
}
const _F = makeValues(color)
//^?
// ZOD SCHEMA, which combines things from above
const colorRequestSchema = z.object({
colorId: z.union(makeValues(color))
});
// USAGE: let's write some code
function getTheme(colorId: string) {
const request = colorRequestSchema.parse({ colorId });
//^?
getThemeApi(request);
}
Update. It finally failed in the end due to the limitation of TS. See: https://github.com/microsoft/TypeScript/issues/34933#issuecomment-1696519995
I believe ZodUnion shouldn't be used for tasks like this one.
It should work: https://stackoverflow.com/a/76797654
In case you have similar problem I had - the trick was to use `${EnumName}`
which resolves to union in TS, and to convince TS Object.values of enum is that union:
const myEnumSchema = z.enum(Object.values(MyEnum) as [`${MyEnum}`]);
This returns a schema, that infers to union of enum values.
for example:
type FruitsUnion = 'Apples' | 'Bananas' | 'Oranges';
enum FruitsEnum {
ApplesWithDifferentKey = 'Apples',
Bananas = 'Bananas',
WhateverSomethingElse = 'Oranges',
}
const fruitsEnumSchema = z.enum(Object.values(FruitsEnum) as [`${FruitsEnum}`]);
// z.ZodEnum<["Apples" | "Bananas" | "Oranges"]>
type FruitsEnumUnion = z.infer<typeof fruitsEnumSchema>;
// "Apples" | "Bananas" | "Oranges"
assert<Equals<FruitsUnion, FruitsEnumUnion>>();
// No error
assert<Equals<'Apples' | 'Bananas', FruitsEnumUnion>>();
assert<Equals<'Apples' | 'Bananas' | 'Oranges' | 'Tomatoes', FruitsEnumUnion>>();
// error
console.log('Parsing result:', fruitsEnumSchema.safeParse('Apples'));
// {success: true, data: 'Apples'}
console.log('Parsing result:', fruitsEnumSchema.safeParse('Tomatoes'));
// {success: false}
Imagine we have an API where some types are list of options. It can be done via enums, arrays or objects. Here's a enum example:
Imagine also, that we have to validate user input. Using Zod we can create a schema:
and then use it for validation:
The problem with this approach is that we're breaking the DRY principle: we have to repeat every value from every enum from the API, which brings even bigger problem with keeping it up to date. If a new color gets added to the enum, how will our schema know about it? There is no way. So the only solid way to do it, is to convert enums to z.unions.
However, I couldn't figure out how to do this.
z.union()
accepts an array of literals, but I cannot find a solution how to createZodUnion
from a enum.Here are a couple of sandboxes, that differ only in the way how options are defined:
Check out the
***
-marked lines.