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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

deny unexpected keys @ ZodObject's .omit(mask),.pick(mask),.required(mask) & .partial(mask) at compile time. #1564

Merged
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