Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal : make coerce more generic #3769

Open
qraynaud opened this issue Sep 23, 2024 · 0 comments
Open

Proposal : make coerce more generic #3769

qraynaud opened this issue Sep 23, 2024 · 0 comments

Comments

@qraynaud
Copy link

qraynaud commented Sep 23, 2024

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 compatible OutputType type with the schema's InputType
  • That method would return a similarly typed ZodSchema except for its InputType that would be changed to the same one as the one of inputSchema
  • The returned schema would basically be the same schema but constructed with the inputSchema in its def object
  • Change the parse method to run it on inputSchema.parse(val) instead of val 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, more complex but more efficient:
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>

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 duration = z.number()
  .or(z.string().min(1))
  .coerce(
    (val: string | number) => typeof val === "string" ? parseDuration : val,
    z.number()
  ); // ZodNumber<string | number> here too

// 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"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant