Skip to content

Commit

Permalink
deny unexpected keys @ ZodObject's .omit(mask),.pick(mask),`.re…
Browse files Browse the repository at this point in the history
…quired(mask)` & `.partial(mask)` at compile time. (#1564)

* deny unexpected keys @ `ZodObject.omit(...)` & `ZodObject.pick(...)`.

* make runtime unit tests ignore unexpected key ts error.

* forgot to run yarn build:deno.

* apply same restrictions for `required(...)` & `partial(...)` masks.

* add "non existent key" tests @ partials & pickomit.

* Naming tweak

---------

Co-authored-by: Colin McDonnell <colinmcd@alum.mit.edu>
Co-authored-by: Colin McDonnell <colinmcd94@gmail.com>
  • Loading branch information
3 people committed Feb 8, 2023
1 parent 17c892a commit 1a033b2
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 17 deletions.
28 changes: 27 additions & 1 deletion deno/lib/__tests__/partials.test.ts
Expand Up @@ -185,22 +185,33 @@ 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(),
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(),
Expand Down Expand Up @@ -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,
});
});
14 changes: 14 additions & 0 deletions deno/lib/__tests__/pickomit.test.ts
Expand Up @@ -103,10 +103,24 @@ test("pick a nonexistent key", () => {

const pickedSchema = schema.pick({
a: true,
// @ts-expect-error should not accept unexpected keys.
doesntExist: true,
});

pickedSchema.parse({
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,
});
});
22 changes: 14 additions & 8 deletions deno/lib/types.ts
Expand Up @@ -1922,6 +1922,12 @@ export type SomeZodObject = ZodObject<
ZodTypeAny
>;

export type objectKeyMask<Obj> = { [k in keyof Obj]?: true };

export type noUnrecognized<Obj extends object, Shape extends object> = {
[k in keyof Obj]: k extends keyof Shape ? Obj[k] : never;
};

function deepPartialify(schema: ZodTypeAny): any {
if (schema instanceof ZodObject) {
const newShape: any = {};
Expand Down Expand Up @@ -2165,8 +2171,8 @@ export class ZodObject<
}) as any;
}

pick<Mask extends { [k in keyof T]?: true }>(
mask: Mask
pick<Mask extends objectKeyMask<T>>(
mask: noUnrecognized<Mask, T>
): ZodObject<Pick<T, Extract<keyof T, keyof Mask>>, UnknownKeys, Catchall> {
const shape: any = {};

Expand All @@ -2182,8 +2188,8 @@ export class ZodObject<
}) as any;
}

omit<Mask extends { [k in keyof T]?: true }>(
mask: Mask
omit<Mask extends objectKeyMask<T>>(
mask: noUnrecognized<Mask, objectKeyMask<T>>
): ZodObject<Omit<T, keyof Mask>, UnknownKeys, Catchall> {
const shape: any = {};

Expand All @@ -2208,8 +2214,8 @@ export class ZodObject<
UnknownKeys,
Catchall
>;
partial<Mask extends { [k in keyof T]?: true }>(
mask: Mask
partial<Mask extends objectKeyMask<T>>(
mask: noUnrecognized<Mask, objectKeyMask<T>>
): ZodObject<
objectUtil.noNever<{
[k in keyof T]: k extends keyof Mask ? ZodOptional<T[k]> : T[k];
Expand Down Expand Up @@ -2241,8 +2247,8 @@ export class ZodObject<
UnknownKeys,
Catchall
>;
required<Mask extends { [k in keyof T]?: true }>(
mask: Mask
required<Mask extends objectKeyMask<T>>(
mask: noUnrecognized<Mask, objectKeyMask<T>>
): ZodObject<
objectUtil.noNever<{
[k in keyof T]: k extends keyof Mask ? deoptional<T[k]> : T[k];
Expand Down
25 changes: 25 additions & 0 deletions src/__tests__/partials.test.ts
Expand Up @@ -184,14 +184,24 @@ 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(),
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);
Expand Down Expand Up @@ -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,
});
});
14 changes: 14 additions & 0 deletions src/__tests__/pickomit.test.ts
Expand Up @@ -102,10 +102,24 @@ test("pick a nonexistent key", () => {

const pickedSchema = schema.pick({
a: true,
// @ts-expect-error should not accept unexpected keys.
doesntExist: true,
});

pickedSchema.parse({
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,
});
});
22 changes: 14 additions & 8 deletions src/types.ts
Expand Up @@ -1922,6 +1922,12 @@ export type SomeZodObject = ZodObject<
ZodTypeAny
>;

export type objectKeyMask<Obj> = { [k in keyof Obj]?: true };

export type noUnrecognized<Obj extends object, Shape extends object> = {
[k in keyof Obj]: k extends keyof Shape ? Obj[k] : never;
};

function deepPartialify(schema: ZodTypeAny): any {
if (schema instanceof ZodObject) {
const newShape: any = {};
Expand Down Expand Up @@ -2165,8 +2171,8 @@ export class ZodObject<
}) as any;
}

pick<Mask extends { [k in keyof T]?: true }>(
mask: Mask
pick<Mask extends objectKeyMask<T>>(
mask: noUnrecognized<Mask, T>
): ZodObject<Pick<T, Extract<keyof T, keyof Mask>>, UnknownKeys, Catchall> {
const shape: any = {};

Expand All @@ -2182,8 +2188,8 @@ export class ZodObject<
}) as any;
}

omit<Mask extends { [k in keyof T]?: true }>(
mask: Mask
omit<Mask extends objectKeyMask<T>>(
mask: noUnrecognized<Mask, objectKeyMask<T>>
): ZodObject<Omit<T, keyof Mask>, UnknownKeys, Catchall> {
const shape: any = {};

Expand All @@ -2208,8 +2214,8 @@ export class ZodObject<
UnknownKeys,
Catchall
>;
partial<Mask extends { [k in keyof T]?: true }>(
mask: Mask
partial<Mask extends objectKeyMask<T>>(
mask: noUnrecognized<Mask, objectKeyMask<T>>
): ZodObject<
objectUtil.noNever<{
[k in keyof T]: k extends keyof Mask ? ZodOptional<T[k]> : T[k];
Expand Down Expand Up @@ -2241,8 +2247,8 @@ export class ZodObject<
UnknownKeys,
Catchall
>;
required<Mask extends { [k in keyof T]?: true }>(
mask: Mask
required<Mask extends objectKeyMask<T>>(
mask: noUnrecognized<Mask, objectKeyMask<T>>
): ZodObject<
objectUtil.noNever<{
[k in keyof T]: k extends keyof Mask ? deoptional<T[k]> : T[k];
Expand Down

0 comments on commit 1a033b2

Please sign in to comment.