diff --git a/README.md b/README.md index a43f263e3..0285de927 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ - [Nullables](#nullables) - [Objects](#objects) - [.shape](#shape) + - [.enum](#enum) - [.extend](#extend) - [.merge](#merge) - [.pick/.omit](#pickomit) @@ -782,6 +783,15 @@ Dog.shape.name; // => string schema Dog.shape.age; // => number schema ``` +### `.keyof` + +Use `.key` to create a `ZodEnum` schema from the keys of an object schema. + +```ts +const keySchema = Dog.keyof(); +keySchema; // ZodEnum<["name", "age"]> +``` + ### `.extend` You can add additional fields to an object schema with the `.extend` method. diff --git a/deno/lib/README.md b/deno/lib/README.md index a43f263e3..0285de927 100644 --- a/deno/lib/README.md +++ b/deno/lib/README.md @@ -63,6 +63,7 @@ - [Nullables](#nullables) - [Objects](#objects) - [.shape](#shape) + - [.enum](#enum) - [.extend](#extend) - [.merge](#merge) - [.pick/.omit](#pickomit) @@ -782,6 +783,15 @@ Dog.shape.name; // => string schema Dog.shape.age; // => number schema ``` +### `.keyof` + +Use `.key` to create a `ZodEnum` schema from the keys of an object schema. + +```ts +const keySchema = Dog.keyof(); +keySchema; // ZodEnum<["name", "age"]> +``` + ### `.extend` You can add additional fields to an object schema with the `.extend` method. diff --git a/deno/lib/__tests__/object.test.ts b/deno/lib/__tests__/object.test.ts index 75a5c1237..b43efccb2 100644 --- a/deno/lib/__tests__/object.test.ts +++ b/deno/lib/__tests__/object.test.ts @@ -238,6 +238,23 @@ test("inferred unioned object type with optional properties", async () => { f1; }); +test("inferred enum type", async () => { + const Enum = z.object({ a: z.string(), b: z.string().optional() }).keyof(); + + expect(Enum.Values).toEqual({ + a: "a", + b: "b", + }); + expect(Enum.enum).toEqual({ + a: "a", + b: "b", + }); + expect(Enum._def.values).toEqual(["a", "b"]); + type Enum = z.infer; + const f1: util.AssertEqual = true; + f1; +}); + test("inferred partial object type with optional properties", async () => { const Partial = z .object({ a: z.string(), b: z.string().optional() }) diff --git a/deno/lib/__tests__/recursive.test.ts b/deno/lib/__tests__/recursive.test.ts index ff472c112..4f359c656 100644 --- a/deno/lib/__tests__/recursive.test.ts +++ b/deno/lib/__tests__/recursive.test.ts @@ -1,6 +1,7 @@ // @ts-ignore TS6133 import { expect } from "https://deno.land/x/expect@v0.2.6/mod.ts"; const test = Deno.test; + import { z } from "../index.ts"; interface Category { diff --git a/deno/lib/helpers/enumUtil.ts b/deno/lib/helpers/enumUtil.ts new file mode 100644 index 000000000..205d26664 --- /dev/null +++ b/deno/lib/helpers/enumUtil.ts @@ -0,0 +1,19 @@ +export namespace enumUtil { + type UnionToIntersectionFn = ( + T extends unknown ? (k: () => T) => void : never + ) extends (k: infer Intersection) => void + ? Intersection + : never; + + type GetUnionLast = UnionToIntersectionFn extends () => infer Last + ? Last + : never; + + type UnionToTuple = [T] extends [never] + ? Tuple + : UnionToTuple>, [GetUnionLast, ...Tuple]>; + + type CastToStringTuple = T extends [string, ...string[]] ? T : never; + + export type UnionToTupleString = CastToStringTuple>; +} diff --git a/deno/lib/types.ts b/deno/lib/types.ts index 684142fdd..727416c7c 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -1,3 +1,4 @@ +import { enumUtil } from "./helpers/enumUtil.ts"; import { errorUtil } from "./helpers/errorUtil.ts"; import { addIssueToContext, @@ -1932,6 +1933,12 @@ export class ZodObject< }) as any; } + keyof(): ZodEnum> { + return createZodEnum( + util.objectKeys(this.shape) as [string, ...string[]] + ) as any; + } + static create = ( shape: T, params?: RawCreateParams diff --git a/src/__tests__/object.test.ts b/src/__tests__/object.test.ts index 100d7a633..de8d5bc93 100644 --- a/src/__tests__/object.test.ts +++ b/src/__tests__/object.test.ts @@ -237,6 +237,23 @@ test("inferred unioned object type with optional properties", async () => { f1; }); +test("inferred enum type", async () => { + const Enum = z.object({ a: z.string(), b: z.string().optional() }).keyof(); + + expect(Enum.Values).toEqual({ + a: "a", + b: "b", + }); + expect(Enum.enum).toEqual({ + a: "a", + b: "b", + }); + expect(Enum._def.values).toEqual(["a", "b"]); + type Enum = z.infer; + const f1: util.AssertEqual = true; + f1; +}); + test("inferred partial object type with optional properties", async () => { const Partial = z .object({ a: z.string(), b: z.string().optional() }) diff --git a/src/__tests__/recursive.test.ts b/src/__tests__/recursive.test.ts index a3c820b52..1c30074d3 100644 --- a/src/__tests__/recursive.test.ts +++ b/src/__tests__/recursive.test.ts @@ -1,5 +1,6 @@ // @ts-ignore TS6133 import { test } from "@jest/globals"; + import { z } from ".."; interface Category { diff --git a/src/helpers/enumUtil.ts b/src/helpers/enumUtil.ts new file mode 100644 index 000000000..205d26664 --- /dev/null +++ b/src/helpers/enumUtil.ts @@ -0,0 +1,19 @@ +export namespace enumUtil { + type UnionToIntersectionFn = ( + T extends unknown ? (k: () => T) => void : never + ) extends (k: infer Intersection) => void + ? Intersection + : never; + + type GetUnionLast = UnionToIntersectionFn extends () => infer Last + ? Last + : never; + + type UnionToTuple = [T] extends [never] + ? Tuple + : UnionToTuple>, [GetUnionLast, ...Tuple]>; + + type CastToStringTuple = T extends [string, ...string[]] ? T : never; + + export type UnionToTupleString = CastToStringTuple>; +} diff --git a/src/types.ts b/src/types.ts index 030a33f16..413e43361 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,4 @@ +import { enumUtil } from "./helpers/enumUtil"; import { errorUtil } from "./helpers/errorUtil"; import { addIssueToContext, @@ -1932,6 +1933,12 @@ export class ZodObject< }) as any; } + keyof(): ZodEnum> { + return createZodEnum( + util.objectKeys(this.shape) as [string, ...string[]] + ) as any; + } + static create = ( shape: T, params?: RawCreateParams