Skip to content

Commit

Permalink
Bugfix: Enum.extract/exclude should not remove error mapping (#3240)
Browse files Browse the repository at this point in the history
When calling .extract/exclude we should retain the original
RawCreateParams passed to the enum definition. We also
allow passing new RawCreateParams if that's necessary.
This solves issue #3225.
  • Loading branch information
shaharke authored Feb 15, 2024
1 parent 5f84473 commit fa90698
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 10 deletions.
5 changes: 3 additions & 2 deletions deno/lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@ There are a growing number of tools that are built atop or support Zod natively!
- [`tRPC`](https://github.com/trpc/trpc): Build end-to-end typesafe APIs without GraphQL.
- [`@anatine/zod-nestjs`](https://github.com/anatine/zod-plugins/tree/main/packages/zod-nestjs): Helper methods for using Zod in a NestJS project.
- [`zod-endpoints`](https://github.com/flock-community/zod-endpoints): Contract-first strictly typed endpoints with Zod. OpenAPI compatible.
- [`zhttp`](https://github.com/evertdespiegeleer/zhttp): An OpenAPI compatible, strictly typed http library with Zod input and response validation.
- [`zhttp`](https://github.com/evertdespiegeleer/zhttp): An OpenAPI compatible, strictly typed http library with Zod input and response validation.
- [`domain-functions`](https://github.com/SeasonedSoftware/domain-functions/): Decouple your business logic from your framework using composable functions. With first-class type inference from end to end powered by Zod schemas.
- [`@zodios/core`](https://github.com/ecyrbe/zodios): A typescript API client with runtime and compile time validation backed by axios and zod.
- [`express-zod-api`](https://github.com/RobinTail/express-zod-api): Build Express-based APIs with I/O schema validation and custom middlewares.
Expand Down Expand Up @@ -587,6 +587,7 @@ There are a growing number of tools that are built atop or support Zod natively!
- [`pastel`](https://github.com/vadimdemedes/pastel): Create CLI applications with react, zod, and ink.
- [`zod-xlsx`](https://github.com/sidwebworks/zod-xlsx): A xlsx based resource validator using Zod schemas.
- [`znv`](https://github.com/lostfictions/znv): Type-safe environment parsing and validation for Node.js with Zod schemas.
- [`zod-config`](https://github.com/alexmarqs/zod-config): Load configurations across multiple sources with flexible adapters, ensuring type safety with Zod.

#### Utilities for Zod

Expand Down Expand Up @@ -964,7 +965,7 @@ You can customize certain error messages when creating a nan schema.
```ts
const isNaN = z.nan({
required_error: "isNaN is required",
invalid_type_error: "isNaN must be not a number",
invalid_type_error: "isNaN must be 'not a number'",
});
```

Expand Down
17 changes: 17 additions & 0 deletions deno/lib/__tests__/enum.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,20 @@ test("extract/exclude", () => {
util.assertEqual<typeof EmptyFoodEnum, z.ZodEnum<[]>>(true);
util.assertEqual<z.infer<typeof EmptyFoodEnum>, never>(true);
});

test('error map in extract/exclude', () => {
const foods = ["Pasta", "Pizza", "Tacos", "Burgers", "Salad"] as const;
const FoodEnum = z.enum(foods, { errorMap: () => ({ message: "This is not food!" }) });
const ItalianEnum = FoodEnum.extract(["Pasta", "Pizza"]);
const foodsError = FoodEnum.safeParse("Cucumbers");
const italianError = ItalianEnum.safeParse("Tacos");
if (!foodsError.success && !italianError.success) {
expect(foodsError.error.issues[0].message).toEqual(italianError.error.issues[0].message);
}

const UnhealthyEnum = FoodEnum.exclude(["Salad"], { errorMap: () => ({ message: "This is not healthy food!" }) });
const unhealthyError = UnhealthyEnum.safeParse("Salad");
if (!unhealthyError.success) {
expect(unhealthyError.error.issues[0].message).toEqual("This is not healthy food!");
}
});
11 changes: 7 additions & 4 deletions deno/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4057,21 +4057,24 @@ export class ZodEnum<T extends [string, ...string[]]> extends ZodType<
}

extract<ToExtract extends readonly [T[number], ...T[number][]]>(
values: ToExtract
values: ToExtract,
newDef: RawCreateParams = this._def
): ZodEnum<Writeable<ToExtract>> {
return ZodEnum.create(values) as any;
return ZodEnum.create(values, newDef) as any;
}

exclude<ToExclude extends readonly [T[number], ...T[number][]]>(
values: ToExclude
values: ToExclude,
newDef: RawCreateParams = this._def
): ZodEnum<
typecast<Writeable<FilterEnum<T, ToExclude[number]>>, [string, ...string[]]>
> {
return ZodEnum.create(
this.options.filter((opt) => !values.includes(opt)) as FilterEnum<
T,
ToExclude[number]
>
>,
newDef
) as any;
}

Expand Down
17 changes: 17 additions & 0 deletions src/__tests__/enum.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,20 @@ test("extract/exclude", () => {
util.assertEqual<typeof EmptyFoodEnum, z.ZodEnum<[]>>(true);
util.assertEqual<z.infer<typeof EmptyFoodEnum>, never>(true);
});

test('error map in extract/exclude', () => {
const foods = ["Pasta", "Pizza", "Tacos", "Burgers", "Salad"] as const;
const FoodEnum = z.enum(foods, { errorMap: () => ({ message: "This is not food!" }) });
const ItalianEnum = FoodEnum.extract(["Pasta", "Pizza"]);
const foodsError = FoodEnum.safeParse("Cucumbers");
const italianError = ItalianEnum.safeParse("Tacos");
if (!foodsError.success && !italianError.success) {
expect(foodsError.error.issues[0].message).toEqual(italianError.error.issues[0].message);
}

const UnhealthyEnum = FoodEnum.exclude(["Salad"], { errorMap: () => ({ message: "This is not healthy food!" }) });
const unhealthyError = UnhealthyEnum.safeParse("Salad");
if (!unhealthyError.success) {
expect(unhealthyError.error.issues[0].message).toEqual("This is not healthy food!");
}
});
11 changes: 7 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4057,21 +4057,24 @@ export class ZodEnum<T extends [string, ...string[]]> extends ZodType<
}

extract<ToExtract extends readonly [T[number], ...T[number][]]>(
values: ToExtract
values: ToExtract,
newDef: RawCreateParams = this._def
): ZodEnum<Writeable<ToExtract>> {
return ZodEnum.create(values) as any;
return ZodEnum.create(values, newDef) as any;
}

exclude<ToExclude extends readonly [T[number], ...T[number][]]>(
values: ToExclude
values: ToExclude,
newDef: RawCreateParams = this._def
): ZodEnum<
typecast<Writeable<FilterEnum<T, ToExclude[number]>>, [string, ...string[]]>
> {
return ZodEnum.create(
this.options.filter((opt) => !values.includes(opt)) as FilterEnum<
T,
ToExclude[number]
>
>,
newDef
) as any;
}

Expand Down

0 comments on commit fa90698

Please sign in to comment.