You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
z.preprocess(fn, schema) lost the input-type narrowing in v4.4.3. #5929 changed the return from ZodPipe<ZodTransform<A, B>, U> to ZodPreprocess<U>, which extends ZodPipe<ZodTransform, U> with ZodTransform used bare — so its I slot defaulted to unknown and core.input<A> came out unknown too, dropping the annotated arg type.
Adds a second I = unknown generic to $ZodPreprocess / ZodPreprocess (and the matching def/internals), threads it into $ZodTransform<unknown, I>, and binds it from the fn arg in the preprocess() factory. Pure type-level change — no runtime behavior shift, optionality plumbing from #5929 is preserved.
Two regression tests in index.test.ts cover the narrowed and unannotated cases.
TL;DR — Restores z.preprocess input-type narrowing that regressed in v4.4.3, where the annotated fn arg type was being collapsed to unknown in z.input<>. Pure type-level fix threading a new I generic through $ZodPreprocess and friends.
Key changes
Thread I generic through $ZodPreprocess — Adds a second I = unknown generic to $ZodPreprocess, $ZodPreprocessDef, and $ZodPreprocessInternals, and plugs it into the inner $ZodTransform<unknown, I> so the input slot is no longer bare.
Bind I from the preprocess() factory fn arg — preprocess<A, U, B>(fn, schema) now returns ZodPreprocess<U, B>, propagating the annotated arg type through to z.input<>.
Before:z.input<typeof z.preprocess((v: string | null | undefined) => ..., z.string())> resolved to unknown. After: it resolves to string | null | undefined, matching the annotated fn arg.
#5929 rewrote z.preprocess to return ZodPreprocess<U> (extending ZodPipe<ZodTransform, U>) instead of ZodPipe<ZodTransform<A, B>, U>. Because $ZodTransform was used bare, its I slot defaulted to unknown and core.input<A> collapsed to unknown, dropping the narrowed arg type that users relied on. Threading a second generic through the preprocess type chain and binding it from fn in the factory recovers the v4.4.2 behavior while keeping the optionality plumbing from #5929 intact.
Why is this type-only?
The runtime construction in `preprocess()` is unchanged — it still builds a `ZodPreprocess` with a `ZodTransform` input and the user schema as output. Only the declared generics on the interfaces and the factory return annotation move.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #5966.
z.preprocess(fn, schema)lost the input-type narrowing in v4.4.3. #5929 changed the return fromZodPipe<ZodTransform<A, B>, U>toZodPreprocess<U>, which extendsZodPipe<ZodTransform, U>withZodTransformused bare — so itsIslot defaulted tounknownandcore.input<A>came outunknowntoo, dropping the annotated arg type.Adds a second
I = unknowngeneric to$ZodPreprocess/ZodPreprocess(and the matching def/internals), threads it into$ZodTransform<unknown, I>, and binds it from thefnarg in thepreprocess()factory. Pure type-level change — no runtime behavior shift, optionality plumbing from #5929 is preserved.Two regression tests in
index.test.tscover the narrowed and unannotated cases.