In order to tackle this problem, I propose a solution involving the combination of three schemas:
import { z } from "zod";
const mandatoryFields = z.object({
id: z.number(),
name: z.string()
});
const stringRegex = /^add_\d{3}_s$/;
const optionalStringFields = z.record(
z.string().regex(stringRegex),
z.string()
);
const numberRegex = /^add_\d{3}_n$/;
const optionalNumberFields = z.record(
z.string().regex(numberRegex),
z.number()
);
While these three schemas form the basis of the desired type, combining them using and
is challenging due to conflicts between the record types and the inability to include mandatory fields in either record. A vanilla TypeScript type for incoming data without an extensive enumerated type would be complex to define.
To overcome this challenge, I suggest using these base schemas to preprocess
the input into a new object that separates the three schema components. This preprocessing does not validate the input but extracts the fields for validation by the final schema:
const schema = z.preprocess(
(args) => {
const unknownRecord = z.record(z.string(), z.unknown()).safeParse(args);
if (!unknownRecord.success) {
return args;
}
const entries = Object.entries(unknownRecord.data);
const numbers = Object.fromEntries(
entries.filter(
([k, v]): [string, unknown] | null => k.match(numberRegex) && [k, v]
)
);
const strings = Object.fromEntries(
entries.filter(
([k, v]): [string, unknown] | null => k.match(stringRegex) && [k, v]
)
);
return {
mandatory: args,
numbers,
strings
};
},
z.object({
mandatory: mandatoryFields,
numbers: optionalNumberFields,
strings: optionalStringFields
})
);
Using this approach, when you input:
const test = schema.parse({
id: 11,
name: "steve",
add_101_s: "cat",
add_123_n: 43,
dont_care: "something"
});
console.log(test);
/* Outputs:
mandatory: Object
id: 11
name: "steve"
numbers: Object
add_123_n: 43
strings: Object
add_101_s: "cat"
*/
You receive separate sections for each component, excluding unnecessary fields like dont_care
, which is an advantage compared to using passthrough
for a similar purpose.
Overall, this method seems optimal unless you wish to develop an extensive optional mapped type for the current records, which could provide better typings but result in a considerably larger file to encompass all fields.