valibot icon indicating copy to clipboard operation
valibot copied to clipboard

feat: create recordWithPatterns schema

Open EskiMojo14 opened this issue 7 months ago • 5 comments

fixes #1159

const Schema = v.recordWithPatterns(
  [
    [
      v.pipe(v.string(), v.regex(/^foo\(.+\)$/)),
      v.pipe(v.string(), v.maxLength(1)),
    ],
    [
      v.pipe(v.string(), v.regex(/^bar\(.+\)$/)),
      v.pipe(v.string(), v.maxLength(10)),
    ],
  ],
  v.number(),
);

Also includes proper index signatures for strongly typed keys, e.g.

const FooKeySchema = v.custom<`foo(${string}`>(
  (arg) => typeof arg === "string" && /^foo\(.+\)$/.test(arg),
);

const Schema = v.recordWithPatterns([[FooKeySchema, v.string()]], v.number());

const parsed = v.parse(Schema, { "foo(x)": "x", x: 1 });

parsed["foo(x)"]; // string
parsed.x; // number

EskiMojo14 avatar Apr 20 '25 21:04 EskiMojo14

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
valibot ✅ Ready (Inspect) Visit Preview 💬 Add feedback Apr 22, 2025 10:11am

vercel[bot] avatar Apr 20 '25 21:04 vercel[bot]

Thank you for creating this PR! Do you know if Zod offers such a schema? How about calling it recordWithPatterns or templateRecord to better match our record schema? Is the second argument, the wildcard schema, necessary? Maybe it simplifies the API if we ask users to add this to the first argument.

fabian-hiller avatar Apr 22 '25 03:04 fabian-hiller

Do you know if Zod offers such a schema?

not to my knowledge, no - JSON schema has patternProperties which is similar

How about calling it recordWithPatterns or templateRecord to better match our record schema?

sure, makes sense

Is the second argument, the wildcard schema, necessary? Maybe it simplifies the API if we ask users to add this to the first argument.

what would be the desired behaviour if a key doesn't match any of the patterns?

EskiMojo14 avatar Apr 22 '25 03:04 EskiMojo14

Ah, the rest argument is necessary, because these two types behave differently:

type Unmerged = {
    [key: `foo(${string})`]: string;
    [key: `bar(${string})`]: number;
} & {
    [key: string]: boolean;
}

type Merged = {
    [key: `foo(${string})`]: string;
    [key: `bar(${string})`]: number;
    [key: string]: boolean;
}

in Merged, the string index signature overrides all of the others.

edit: hmm, the below works, so we could just avoid calling Prettify to merge the intersection:

type Unmerged = {
    [key: `foo(${string})`]: string;
} & {
    [key: `bar(${string})`]: number;
} & {
    [key: string]: boolean;
}

it's kinda ugly, but 🤷🏻

EskiMojo14 avatar Apr 22 '25 10:04 EskiMojo14

that does still leave the question of what to do when a key doesn't match any of the patterns:

const parsed = v.parse(
  v.recordWithPatterns([
    [v.pipe(v.string(), v.startsWith("foo-")), v.string()],
  ]),
  { "foo-allowed": "hi", notAllowed: true },
);
// loose - { 'foo-allowed': 'hi', notAllowed: true }
// strip - { 'foo-allowed': 'hi' }
// strict - error
// rest - validate against rest, error if not match

EskiMojo14 avatar Apr 22 '25 16:04 EskiMojo14