Skip to content

fix(v4): preserve z.preprocess input narrowing#5967

Open
devareddy05 wants to merge 1 commit into
colinhacks:mainfrom
devareddy05:fix/preprocess-input-narrowing
Open

fix(v4): preserve z.preprocess input narrowing#5967
devareddy05 wants to merge 1 commit into
colinhacks:mainfrom
devareddy05:fix/preprocess-input-narrowing

Conversation

@devareddy05
Copy link
Copy Markdown

Closes #5966.

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.

@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog Bot commented May 7, 2026

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 argpreprocess<A, U, B>(fn, schema) now returns ZodPreprocess<U, B>, propagating the annotated arg type through to z.input<>.
  • Regression tests for Narrowing the input type of z.preprocess no longer works after updating to latest version (v4.4.3) #5966 — Two expectTypeOf cases in index.test.ts cover both the annotated arg case (string | null | undefined) and the unannotated fallback (unknown).

Summary | 3 files | 1 commit | base: mainfix/preprocess-input-narrowing


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.

packages/zod/src/v4/classic/schemas.ts · packages/zod/src/v4/core/schemas.ts · packages/zod/src/v4/classic/tests/index.test.ts

Pullfrog  | View workflow run | via Pullfrog | Using Claude Opus𝕏

Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed — no issues found.

Pullfrog  | View workflow run | Using Claude Opus𝕏

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

Successfully merging this pull request may close these issues.

Narrowing the input type of z.preprocess no longer works after updating to latest version (v4.4.3)

1 participant