zod
zod copied to clipboard
Proposal : make `coerce` more generic
Hi!
I've been thinking about how to make coerce
something more powerful and generic than what it is right now and I think I found an elegant solution. Not knowing zod
codebase perfectly (I read a big part of it though) I might be missing things that would make this impossible. But I think it should work.
1st, add to every zod schema a schema.constructWith(inputSchema)
method (that name can obviously be changed, I'm not sure it is good) that would satisfy the followings:
-
inputSchema
REQUIRES a compatibleOutputType
type with the schema'sInputType
- That method would return a similarly typed
ZodSchema
except for itsInputType
that would be changed to the same one as the one ofinputSchema
- The returned schema would basically be the same
schema
but constructed with theinputSchema
in its def object - Change the parse method to run it on
inputSchema.parse(val)
instead ofval
directly when there is one
2nd add a schema.coerce(transformFn, outputSchema)
method to every zod schema that would basically just return outputSchema.constructWith(schema.transform(transformFn))
.
Those changes would allow to implement existing coerce tooling more easily:
zod.coerce.number = z.string()
.or(z.number())
// that might not be enough to put Number only there, but it keeps things simple
.coerce(Number, z.number());
// or:
zod.coerce.number = z.number()
.constructWith(
z.number()
.or(z.string().transform(Number))
);
// ZodNumber would need to be parameterized with its input type now
// eg: ZodNumber<Input = number> extends ZodType<number, ZodNumberDef, Input> {}
// zod.coerce.number would be defined as a ZodNumber<string | number>
If we want to keep very broad coercing tooling, it could be defined as:
zod.coerce.number = z.unknown()
.coerce(Number, z.number()); // ZodNumber<unknown>
I think that would also allow to completely deprecate z.preprocess()
that would be better served by using .coerce()
or .constructWith()
(it could always be simulated using z.unknown().coerce()
, the only difference being that the resulting schema would be "better" typed).
Some very cool uses for this would be to create some generic schemas representing more complex things that can still be used in a very intuitive way after that:
// duration.ts
import parseDuration from "parse-duration";
export const duration = z.string()
.min(1)
.or(z.number())
.coerce(
(val: string | number) => typeof val === "string" ? parseDuration(val) : val,
z.number()
) // another ZodNumber<string | number>
// example.ts
import { duration } from "./duration.js";
const requestTimeout = duration
.int("do not support µs")
.min(1_000, "1s minimum timeout")
;
// ...
requestTimeout.parse("20s"); // OK => 20_000
requestTimeout.parse("200ms"); // fails with "1s minimum timeout"
requestTimeout.parse(10_000); // OK => 10_000
requestTimout.parse(10_000.25); // fails with "do not support µs"