Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to make ZodUnion from a enum? #2691

Open
OnkelTem opened this issue Aug 28, 2023 · 4 comments
Open

How to make ZodUnion from a enum? #2691

OnkelTem opened this issue Aug 28, 2023 · 4 comments

Comments

@OnkelTem
Copy link

OnkelTem commented Aug 28, 2023

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:

enum color {
  red = "red",
  green = "green",
};

Imagine also, that we have to validate user input. Using Zod we can create a schema:

const colorSchema = z.union([z.literal("red"), z.literal("green")])

and then use it for validation:

const color = colorSchema.parse(userInput)
      //^? color: "red" | "green"

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 create ZodUnion from a enum.

Here are a couple of sandboxes, that differ only in the way how options are defined:

Check out the ***-marked lines.

@OnkelTem
Copy link
Author

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);
}

@OnkelTem
Copy link
Author

Update. It finally failed in the end due to the limitation of TS.
See: microsoft/TypeScript#34933 (comment)

I believe ZodUnion shouldn't be used for tasks like this one.

@Nishchit14
Copy link

It should work: https://stackoverflow.com/a/76797654

@andrienko
Copy link

andrienko commented Sep 11, 2024

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}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants