zod
zod copied to clipboard
Preprocess breaks relationship between TypeOf<T>/infer<T> and ZodType<T>
Given a generic T, I'm trying to type the result of a function as the schema which can create T. This breaks however if the schema has a preprocessed field. I've broken the issue down to:
import { z, ZodType, ZodtypeDef } from 'zod'
const noPreprocessObjSchema = z.object({
date: z.date(),
});
type NoPreprocessObj = z.infer<typeof noPreprocessObjSchema>;
const preprocessObjSchema = z.object({
date: z.preprocess(
(arg) => (typeof arg === 'string' ? new Date(arg) : arg),
z.date(),
),
});
type PreprocessObj = z.infer<typeof preprocessObjSchema>;
const noPreprocessParser: z.ZodType<NoPreprocessObj> = noPreprocessObjSchema;
const preprocessParser: z.ZodType<PreprocessObj> = preprocessObjSchema; // Error: The types of '_input.date' are incompatible between these types. Type 'unknown' is not assignable to type 'Date'
Is the recommend solution that I type the prepocessor as ZodType<Date, ZodTypeDef, Date>
?
Wanted to bump this since I see its getting a little attention but no answers. I see this change was implemented rather specifically on September 11th. Normally for a primitive parser like string()
the zod type would have both its input and output marked as string
correct?
ZodString extends ZodType<string, ZodStringDef>
...
ZodType<Output = any, Def extends ZodTypeDef = ZodTypeDef, Input = Output>
But even with Input typed as string, the .parse
and .safeParse
methods both accept unknown
as the parse argument. So I'm not sure what value having .preprocesses
input as unknown
brings. Conceptually, yeah anything could go into a preprocessor, but the same could be said for the primitive parsers. I'd expect them all to fail if the value can't be parsed.
@ConnorSinnott This is partially correct. Read until the end because there's a PR to fix this.
When you create a schema with preprocess
, there's no way of knowing which input you will receive on runtime. I mean,
date: z.preprocess(
(arg) => (typeof arg === 'string' ? new Date(arg) : arg),
z.date(),
),
How can you be sure that arg
will be a Date
object? You have even added a typeof arg === 'string'
check that proves that uncertainty.
So, Input
will be unknown
.
However, we can fix preprocess
to enable such thing.
Please take a look at this PR.
@ConnorSinnott Is this what you are looking for?
const schema = z.object( {
date: z.preprocess(
arg => ( typeof arg === 'string' ? new Date( arg ) : arg ),
z.date(),
),
} )
type Obj = z.infer<typeof schema>
// type Obj = {
// date: Date
// }
const typeCheck: z.ZodType<Obj, z.ZodTypeDef, { date?: unknown }> = schema
// no ts errors, yay!
console.log( schema.safeParse( { date: '2023-01-01' } ).success ) // true
console.log( schema.safeParse( { date: '01/01/2023' } ).success ) // true
console.log( schema.safeParse( { date: new Date() } ).success ) // true
console.log( schema.safeParse( { date: 42 } ).success ) // false
console.log( schema.safeParse( { date: 'foo' } ).success ) // false
Hey @JacobWeisenburger! My issue isn't with the parsing behavior, but with the types generated by z.infer
and how they related to the schemas. @santosmarco-caribou answer resonates more closely, and actually in the PR makes similar points. Ultimately the schemas I've defined in OP expect a Date, and just because I have some logic to coerce an input, doesn't change that I'm ultimately looking for a date. I'd expect the inferred types to reflect this.
Is there any chance https://github.com/colinhacks/zod/commit/2893e1659875944198aff28a3ba404858f834ed8 could be reverted to address this?
@franzwilhelm there's discussion about that in 1752
Thank you @ConnorSinnott