Skip to content

Commit 762e911

Browse files
committed
Generalize numeric key handling
1 parent dfbbf1c commit 762e911

3 files changed

Lines changed: 40 additions & 15 deletions

File tree

packages/zod/src/v4/classic/tests/record.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,38 @@ test("partialRecord with z.literal([key, ...])", () => {
487487
`);
488488
});
489489

490+
test("partialRecord with numeric literal keys", () => {
491+
const Keys = z.literal([1, 2, 3]);
492+
const schema = z.partialRecord(Keys, z.string());
493+
type Schema = z.infer<typeof schema>;
494+
expectTypeOf<Schema>().toEqualTypeOf<Partial<Record<1 | 2 | 3, string>>>();
495+
496+
// Should parse valid partials with numeric keys (as strings in JS objects)
497+
expect(schema.parse({})).toEqual({});
498+
expect(schema.parse({ 1: "one" })).toEqual({ 1: "one" });
499+
expect(schema.parse({ 2: "two", 3: "three" })).toEqual({ 2: "two", 3: "three" });
500+
501+
// Should fail with unrecognized key
502+
expect(schema.safeParse({ 4: "four" }).success).toBe(false);
503+
});
504+
505+
test("partialRecord with union of string and numeric literal keys", () => {
506+
const StringKeys = z.literal(["a", "b", "c"]);
507+
const NumericKeys = z.literal([1, 2, 3]);
508+
const schema = z.partialRecord(z.union([StringKeys, NumericKeys]), z.string());
509+
type Schema = z.infer<typeof schema>;
510+
expectTypeOf<Schema>().toEqualTypeOf<Partial<Record<"a" | "b" | "c" | 1 | 2 | 3, string>>>();
511+
512+
// Should parse valid partials with mixed keys
513+
expect(schema.parse({})).toEqual({});
514+
expect(schema.parse({ a: "1", 2: "4" })).toEqual({ a: "1", 2: "4" });
515+
expect(schema.parse({ a: "a", b: "b", 1: "1", 2: "2" })).toEqual({ a: "a", b: "b", 1: "1", 2: "2" });
516+
517+
// Should fail with unrecognized key
518+
expect(schema.safeParse({ d: "d" }).success).toBe(false);
519+
expect(schema.safeParse({ 4: "4" }).success).toBe(false);
520+
});
521+
490522
test("looseRecord passes through non-matching keys", () => {
491523
const schema = z.looseRecord(z.string().regex(/^S_/), z.string());
492524

packages/zod/src/v4/core/schemas.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2804,14 +2804,9 @@ export const $ZodRecord: core.$constructor<$ZodRecord> = /*@__PURE__*/ core.$con
28042804
throw new Error("Async schemas not supported in object keys currently");
28052805
}
28062806

2807-
// Numeric string fallback: if key failed with "expected number", retry with Number(key)
2808-
const checkNumericKey =
2809-
typeof key === "string" &&
2810-
regexes.number.test(key) &&
2811-
keyResult.issues.length &&
2812-
keyResult.issues.some(
2813-
(iss) => iss.code === "invalid_type" && (iss as errors.$ZodIssueInvalidType).expected === "number"
2814-
);
2807+
// Numeric string fallback: if key is a numeric string and failed, retry with Number(key)
2808+
// This handles z.number(), z.literal([1, 2, 3]), and unions containing numeric literals
2809+
const checkNumericKey = typeof key === "string" && regexes.number.test(key) && keyResult.issues.length;
28152810
if (checkNumericKey) {
28162811
const retryResult = def.keyType._zod.run({ value: Number(key), issues: [] }, ctx);
28172812
if (retryResult instanceof Promise) {

play.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import * as z from "./packages/zod/src/v4/index.js";
1+
import { z } from "zod";
22

3-
z;
3+
const type1 = z.literal(["a", "b", "c"]);
4+
const type2 = z.literal([1, 2, 3]);
45

5-
const schema = z.object({ name: z.string() }).and(z.looseRecord(z.string().regex(/_phone$/), z.e164()));
6+
const record = z.partialRecord(z.union([type1, type2]), z.string());
67

7-
type _schema = z.infer<typeof schema>;
8-
// { name: string } & Record<string, string> & Record<string, number>
9-
10-
z.e164();
8+
console.log(record.parse({ a: "1", 2: "4" }));

0 commit comments

Comments
 (0)