zod icon indicating copy to clipboard operation
zod copied to clipboard

How to make ZodUnion from a enum?

Open OnkelTem opened this issue 2 years ago • 4 comments

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 avatar Aug 28 '23 16:08 OnkelTem

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 avatar Aug 28 '23 21:08 OnkelTem

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.

OnkelTem avatar Aug 29 '23 09:08 OnkelTem

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

Nishchit14 avatar Jun 20 '24 13:06 Nishchit14

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}

andrienko avatar Sep 11 '24 14:09 andrienko

Hi, @OnkelTem. I'm Dosu, and I'm helping the Zod team manage their backlog. I'm marking this issue as stale.

Issue Summary:

  • You raised an issue about maintaining DRY principles with Zod and TypeScript enums.
  • Initially found a solution with help from the TypeScript Discord, but it failed due to TypeScript limitations.
  • Nishchit14 suggested a Stack Overflow solution.
  • Andrienko provided an alternative approach using template literals and Object.values.
  • Andrienko's solution was well-received and effectively resolved the issue.

Next Steps:

  • Please let me know if this issue is still relevant to the latest version of the Zod repository. You can keep the discussion open by commenting on the issue.
  • If there are no further updates, the issue will be automatically closed in 14 days.

Thank you for your understanding and contribution!

dosubot[bot] avatar Jul 24 '25 16:07 dosubot[bot]