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

Restrict z.enum options to existing type #2280

Closed
IlyaSemenov opened this issue Mar 31, 2023 · 6 comments · Fixed by #2338
Closed

Restrict z.enum options to existing type #2280

IlyaSemenov opened this issue Mar 31, 2023 · 6 comments · Fixed by #2338

Comments

@IlyaSemenov
Copy link
Contributor

IlyaSemenov commented Mar 31, 2023

Problem

I want to restrict options passed to z.enum to the existing type.

type Status = 'new' | 'accepted' | 'closed'

z.enum(['new', 'accepted']) // OK
z.enum(['new', 'approved']) // Invalid choice - should be somehow disallowed

Is there an elegant way to statically validate the typings of enum options?

Solutions tried

z.enum(['new', 'accepted'] as Status[]) // Argument of type 'Status[]' is not assignable to parameter of type '[string, ...string[]]'.
z.enum(['new', 'accepted'] satisfies Status[]) // Argument of type '("new" | "acepted")[]' is not assignable to parameter of type '[string, ...string[]]'.
z.enum<Status>(['new', 'accepted']) // Expected 2 type arguments, but got 1
z.enum(['new', 'accepted'] as [Status, ...Status[]]) // works, but ugly
z.enum(['new', 'accepted'] satisfies [Status, ...Status[]]) // works, but ugly
z.enum(['new', 'accepted'] as const satisfies readonly Status[]) // works, but could it be simpler?

at least a built-in helper would be useful:

z.enum(['new', 'accepted'] as z.EnumOptions<Status>) // self explanatory
@IlyaSemenov
Copy link
Contributor Author

IlyaSemenov commented Mar 31, 2023

Perhaps we can update EnumValues:

zod/src/types.ts

Line 3918 in 51e14be

export type EnumValues = [string, ...string[]];

into this?

export type EnumValues<T extends string = string> = [T, ...T[]];

@IlyaSemenov
Copy link
Contributor Author

IlyaSemenov commented Apr 11, 2023

Another option would be to update EnumValues to be less restrictive:

 export type EnumValues = string[];  // and similar updates in createEnum()

then the validation works just fine with simple satisifes test:

type Status = 'new' | 'accepted' | 'closed'

z.enum(['new', 'accepted'] satisfies Status[]) // OK
z.enum(['new', 'approved'] satisfies Status[]) // Error: Type 'string' does not satisfy the expected type 'Status[]'

In #2312, @igalklebanov told that allowing string[] was a footgun. May be it is, but I am not seeing how exactly so? After all, z.enum([]) is not a completely impossible scenario. Why would the user be disallowed to create a schema that knowingly never validates?

IlyaSemenov added a commit to IlyaSemenov/zod that referenced this issue Apr 18, 2023
Allow statically validate enum values against external type.
Fix colinhacks#2280.
@IlyaSemenov
Copy link
Contributor Author

Making EnumValues generic (see linked PR) is fully backwards compatible.

From what I have discovered [string, ...string[]] seems to be currently working not only to require at least one value but also as some kind of poor-man alternative to Typescript 5.0 const generic modifier and blindly replacing it with string[] will create a regression for inferred types. Hopefully the maintainers will decide to bump to TS 5 at some point in future.

@stale
Copy link

stale bot commented Jul 17, 2023

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale No activity in last 60 days label Jul 17, 2023
@IlyaSemenov
Copy link
Contributor Author

There is a PR for implementing this improve which makes no harm.

@stale stale bot removed the stale No activity in last 60 days label Jul 26, 2023
@colinhacks
Copy link
Owner

Passing a generic into z.enum<>() breaks inference. Before const parameters it used to require two generics to properly infer a tuple of string literals. In Zod 4 I'll bump the minimum version to 5.0 and there will be a more convenient way to do this.

Generally speaking, you aren't supposed to start with a type then try to write a Zod schema that's compatible with it. The goal is to define your types with Zod, so it's often inconvenient to go the other direction. This is one of those cases.

In the meantime I recommend using your own Tuple helper. It's not perfect but it works.

type Status = "pending" | "fulfilled" | "rejected";
type Tuple<T extends string> = [T, ...T[]];

const myEnum: z.ZodEnum<Tuple<Status>> = z.enum([
  "pending",
  "fulfilled",
  "rejected",
]);

colinhacks added a commit that referenced this issue Apr 9, 2024
* feat: make EnumValues generic

Allow statically validate enum values against external type.
Fix #2280.

* Docs

---------

Co-authored-by: Colin McDonnell <[email protected]>
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.

2 participants