diff --git a/deno/lib/__tests__/partials.test.ts b/deno/lib/__tests__/partials.test.ts index 5f6947a90..bb6eb355f 100644 --- a/deno/lib/__tests__/partials.test.ts +++ b/deno/lib/__tests__/partials.test.ts @@ -185,6 +185,15 @@ test("required with mask", () => { expect(requiredObject.shape.country).toBeInstanceOf(z.ZodOptional); }); + +test("required with mask containing a nonexistent key", () => { + object.required({ + age: true, + // @ts-expect-error should not accept unexpected keys. + doesntExist: true, + }); +}); + test("required with mask -- ignore falsy values", () => { const object = z.object({ name: z.string(), @@ -192,15 +201,17 @@ test("required with mask -- ignore falsy values", () => { field: z.string().optional().default("asdf"), country: z.string().optional(), }); - + // @ts-expect-error const requiredObject = object.required({ age: true, country: false }); expect(requiredObject.shape.name).toBeInstanceOf(z.ZodString); expect(requiredObject.shape.age).toBeInstanceOf(z.ZodNumber); expect(requiredObject.shape.field).toBeInstanceOf(z.ZodDefault); expect(requiredObject.shape.country).toBeInstanceOf(z.ZodOptional); + }); + test("partial with mask", async () => { const object = z.object({ name: z.string(), @@ -241,3 +252,18 @@ test("partial with mask -- ignore falsy values", async () => { masked.parse({ country: "US" }); await masked.parseAsync({ country: "US" }); }); + +test("partial with mask containing a nonexistent key", () => { + const object = z.object({ + name: z.string(), + age: z.number().optional(), + field: z.string().optional().default("asdf"), + country: z.string().optional(), + }); + + object.partial({ + age: true, + // @ts-expect-error should not accept unexpected keys. + doesntExist: true, + }); +}); diff --git a/deno/lib/__tests__/pickomit.test.ts b/deno/lib/__tests__/pickomit.test.ts index a350c6d0f..d2eb28848 100644 --- a/deno/lib/__tests__/pickomit.test.ts +++ b/deno/lib/__tests__/pickomit.test.ts @@ -103,6 +103,7 @@ test("pick a nonexistent key", () => { const pickedSchema = schema.pick({ a: true, + // @ts-expect-error should not accept unexpected keys. doesntExist: true, }); @@ -110,3 +111,16 @@ test("pick a nonexistent key", () => { a: "value", }); }); + +test("omit a nonexistent key", () => { + const schema = z.object({ + a: z.string(), + b: z.number(), + }); + + schema.omit({ + a: true, + // @ts-expect-error should not accept unexpected keys. + doesntExist: true, + }); +}); diff --git a/deno/lib/types.ts b/deno/lib/types.ts index 4d8d18c8c..6711aa9eb 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -1922,6 +1922,12 @@ export type SomeZodObject = ZodObject< ZodTypeAny >; +export type objectKeyMask = { [k in keyof Obj]?: true }; + +export type noUnrecognized = { + [k in keyof Obj]: k extends keyof Shape ? Obj[k] : never; +}; + function deepPartialify(schema: ZodTypeAny): any { if (schema instanceof ZodObject) { const newShape: any = {}; @@ -2165,8 +2171,8 @@ export class ZodObject< }) as any; } - pick( - mask: Mask + pick>( + mask: noUnrecognized ): ZodObject>, UnknownKeys, Catchall> { const shape: any = {}; @@ -2182,8 +2188,8 @@ export class ZodObject< }) as any; } - omit( - mask: Mask + omit>( + mask: noUnrecognized> ): ZodObject, UnknownKeys, Catchall> { const shape: any = {}; @@ -2208,8 +2214,8 @@ export class ZodObject< UnknownKeys, Catchall >; - partial( - mask: Mask + partial>( + mask: noUnrecognized> ): ZodObject< objectUtil.noNever<{ [k in keyof T]: k extends keyof Mask ? ZodOptional : T[k]; @@ -2241,8 +2247,8 @@ export class ZodObject< UnknownKeys, Catchall >; - required( - mask: Mask + required>( + mask: noUnrecognized> ): ZodObject< objectUtil.noNever<{ [k in keyof T]: k extends keyof Mask ? deoptional : T[k]; diff --git a/src/__tests__/partials.test.ts b/src/__tests__/partials.test.ts index bdf43a85a..448bdb22c 100644 --- a/src/__tests__/partials.test.ts +++ b/src/__tests__/partials.test.ts @@ -184,7 +184,16 @@ test("required with mask", () => { expect(requiredObject.shape.country).toBeInstanceOf(z.ZodOptional); }); +test("required with mask containing a nonexistent key", () => { + object.required({ + age: true, + // @ts-expect-error should not accept unexpected keys. + doesntExist: true, + }); +}); + test("required with mask -- ignore falsy values", () => { + const object = z.object({ name: z.string(), age: z.number().optional(), @@ -192,6 +201,7 @@ test("required with mask -- ignore falsy values", () => { country: z.string().optional(), }); + // @ts-expect-error const requiredObject = object.required({ age: true, country: false }); expect(requiredObject.shape.name).toBeInstanceOf(z.ZodString); @@ -240,3 +250,18 @@ test("partial with mask -- ignore falsy values", async () => { masked.parse({ country: "US" }); await masked.parseAsync({ country: "US" }); }); + +test("partial with mask containing a nonexistent key", () => { + const object = z.object({ + name: z.string(), + age: z.number().optional(), + field: z.string().optional().default("asdf"), + country: z.string().optional(), + }); + + object.partial({ + age: true, + // @ts-expect-error should not accept unexpected keys. + doesntExist: true, + }); +}); diff --git a/src/__tests__/pickomit.test.ts b/src/__tests__/pickomit.test.ts index 1a41e52c4..a6215a42c 100644 --- a/src/__tests__/pickomit.test.ts +++ b/src/__tests__/pickomit.test.ts @@ -102,6 +102,7 @@ test("pick a nonexistent key", () => { const pickedSchema = schema.pick({ a: true, + // @ts-expect-error should not accept unexpected keys. doesntExist: true, }); @@ -109,3 +110,16 @@ test("pick a nonexistent key", () => { a: "value", }); }); + +test("omit a nonexistent key", () => { + const schema = z.object({ + a: z.string(), + b: z.number(), + }); + + schema.omit({ + a: true, + // @ts-expect-error should not accept unexpected keys. + doesntExist: true, + }); +}); diff --git a/src/types.ts b/src/types.ts index b94201125..88805285a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1922,6 +1922,12 @@ export type SomeZodObject = ZodObject< ZodTypeAny >; +export type objectKeyMask = { [k in keyof Obj]?: true }; + +export type noUnrecognized = { + [k in keyof Obj]: k extends keyof Shape ? Obj[k] : never; +}; + function deepPartialify(schema: ZodTypeAny): any { if (schema instanceof ZodObject) { const newShape: any = {}; @@ -2165,8 +2171,8 @@ export class ZodObject< }) as any; } - pick( - mask: Mask + pick>( + mask: noUnrecognized ): ZodObject>, UnknownKeys, Catchall> { const shape: any = {}; @@ -2182,8 +2188,8 @@ export class ZodObject< }) as any; } - omit( - mask: Mask + omit>( + mask: noUnrecognized> ): ZodObject, UnknownKeys, Catchall> { const shape: any = {}; @@ -2208,8 +2214,8 @@ export class ZodObject< UnknownKeys, Catchall >; - partial( - mask: Mask + partial>( + mask: noUnrecognized> ): ZodObject< objectUtil.noNever<{ [k in keyof T]: k extends keyof Mask ? ZodOptional : T[k]; @@ -2241,8 +2247,8 @@ export class ZodObject< UnknownKeys, Catchall >; - required( - mask: Mask + required>( + mask: noUnrecognized> ): ZodObject< objectUtil.noNever<{ [k in keyof T]: k extends keyof Mask ? deoptional : T[k];