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

Preprocess and parser Input type #1405

Closed
sbking opened this issue Sep 11, 2022 · 2 comments
Closed

Preprocess and parser Input type #1405

sbking opened this issue Sep 11, 2022 · 2 comments

Comments

@sbking
Copy link
Contributor

sbking commented Sep 11, 2022

When using z.preprocess, the parser's Input type is set to the wrapped parser's Input type. In most cases, this is probably incorrect, and this prevents using z.preprocess with frameworks like tRPC without type errors.

Example:

const parser = z.preprocess((data) => Number(data), z.number());

function handle(data: z.input<typeof parser>) {
  return parser.parse(data);
}

handle("42");
// ts-error: Argument of type 'string' is not assignable to parameter of type 'number'.

^ That's a very simplified example of how this works in tRPC. I'm not sure what the ideal API is here. Maybe if we could explicitly annotate the preprocess argument? e.g.:

const parser = z.preprocess((data: string | number) => Number(data), z.number());

type Input = z.input<typeof parser>;
assertEqual<Input, string | number>(true);

Or alternatively, should preprocess simply always have an Input type of unknown? This seems like it would make the most sense, because using preprocess assumes you can handle any unknown value.

const parser = z.preprocess((data) => Number(data), z.number());

type Input = z.input<typeof parser>;
assertEqual<Input, unknown>(true);
@sbking
Copy link
Contributor Author

sbking commented Sep 11, 2022

Thinking about this some more, I think a better solution would be to have some kind of chain API that allows you to pipe the result of one parser to another parser:

const numberParser = z.preprocess(Number, z.number().int().nonnegative());
const stringParser = z.string().chain(numberParser).transform((n) => [n]);

// Input: string
// Output: number[]

This would be essentially equivalent to:

const parser = z
  .string()
  .transform(Number)
  .refine((n) => z.number().int().nonnegative().safeParse(n).success)
  .transform((n) => [n]);

However using refine and safeParse is really tedious, and refine has incorrect types when used with type guards - see #1404

In fact I think a chain API would support all use cases for preprocess but in a more generic way:

const parser = z.unknown().transform(Number).chain(z.number().int().nonnegative());

@colinhacks
Copy link
Owner

Or alternatively, should preprocess simply always have an Input type of unknown?

This is definitely what it should be, updated in 3.19.1.

The chain API is on the roadmap (in my brain) and is discussed elsewhere so I'm gonna close this 👍

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

2 participants