Skip to content

Commit 56222cd

Browse files
authored
feat(instanceof): key the .properties() shape off the instance type (#6600)
The method took any $ZodShape, so a misspelled key or a schema that can't accept the property's type both compiled. The shape is now a mapped type over keyof T, which also makes the keys autocomplete. Methods are excluded from the shape. Every object literal inherits Object.prototype.toString, so a constraint carrying URL's own toString entry rejects every shape literal passed to it. Type-only: the emitted JS is byte-identical, so runtime, memory and bundle are all unchanged.
1 parent de65a5c commit 56222cd

2 files changed

Lines changed: 21 additions & 1 deletion

File tree

‎packages/zod/src/v4/classic/schemas.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2841,7 +2841,8 @@ type ZodInstanceOfParams = core.Params<
28412841

28422842
// ZodInstanceOf
28432843
export interface ZodInstanceOf<T = unknown> extends ZodCustom<T, T> {
2844-
properties<Shape extends core.$ZodShape>(
2844+
// the shape is keyed off the instance type, so keys autocomplete and a schema that can't accept the property's type is an error. Methods are excluded: every object literal inherits Object.prototype.toString, which would otherwise collide with the constraint's own toString entry and reject every shape.
2845+
properties<Shape extends { [k in keyof T as T[k] extends Function ? never : k]?: core.$ZodType<unknown, T[k]> }>(
28452846
shape: Shape,
28462847
params?: string | core.$ZodCheckPropertiesParams
28472848
): ZodInstanceOf<T & core.$InferObjectInput<Shape, {}>>;

‎packages/zod/src/v4/classic/tests/properties.test.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,25 @@ test("z.instanceof().properties()", () => {
5151
expectTypeOf<z.infer<typeof W>>().toEqualTypeOf<URL & { search: string }>();
5252
});
5353

54+
test("z.instanceof().properties() constrains the shape to the instance type", () => {
55+
const file = z.instanceof(File).properties({ name: z.string(), size: z.number().max(1024) });
56+
expectTypeOf<z.infer<typeof file>>().toEqualTypeOf<File & { name: string; size: number }>();
57+
58+
// @ts-expect-error no such property on File
59+
z.instanceof(File).properties({ nmae: z.string() });
60+
61+
// @ts-expect-error File["size"] is a number
62+
z.instanceof(File).properties({ size: z.string() });
63+
64+
// methods are out of the shape, so an object literal's inherited toString can't collide with the constraint
65+
// @ts-expect-error
66+
z.instanceof(File).properties({ text: z.any() });
67+
68+
// a literal still narrows a wider property
69+
const png = z.instanceof(File).properties({ type: z.literal("image/png") });
70+
expectTypeOf<z.infer<typeof png>>().toEqualTypeOf<File & { type: "image/png" }>();
71+
});
72+
5473
test("z.properties spreads into .check()", () => {
5574
// the 4.5 array call sites: the check yields itself from Symbol.iterator
5675
const p = z.properties({ a: z.literal("x") });

0 commit comments

Comments
 (0)