Skip to content

Fix classic pipe compatibility for branded outputs#5914

Open
colinhacks wants to merge 2 commits intomainfrom
issue-5648-pipe-brand
Open

Fix classic pipe compatibility for branded outputs#5914
colinhacks wants to merge 2 commits intomainfrom
issue-5648-pipe-brand

Conversation

@colinhacks
Copy link
Copy Markdown
Owner

Fixes #5648.

This keeps the existing strict .pipe() overload for contextual typing, then adds a Classic method fallback that checks pipe compatibility in the direction the runtime uses: the source output must be assignable to the target input. That lets branded outputs pipe into schemas that accept the corresponding unbranded input while preserving the existing contextual typing behavior for transforms.

Validated with pnpm exec tsc -p packages/zod/tsconfig.test.json --noEmit and pnpm vitest run packages/zod/src/v4/classic/tests/pipe.test.ts.

@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog Bot commented Apr 30, 2026

New pull request. Leaping into action...

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

@dokson
Copy link
Copy Markdown
Contributor

dokson commented May 1, 2026

Heads-up: this PR also resolves #5694 (and effectively #4778). The new overload's core.output<this> extends core.input<T> check covers both the "branded output → unbranded input" case in your tests and the "stricter source → looser target" case (z.number().pipe(z.number().optional()), .transform().pipe(nullable_schema)) reported there.

Verified locally: both #5694 reproductions fail on main with the documented TypeCheckError, and pass on this branch with no extra changes. If it'd help reviewer confidence to lock in those cases too, here's a drop-in test file you can include — feel free to ignore if you'd rather keep the PR scope narrow.

// packages/zod/src/v4/classic/tests/pipe-compat.test.ts (or merge into pipe.test.ts)
import { expectTypeOf, test } from "vitest";
import * as z from "zod/v4";

test("pipe stricter source into looser target (issue #5694)", () => {
  const maybeNumber = z.number().optional();
  const out = z.number().pipe(maybeNumber).parse(42);
  expectTypeOf(out).toEqualTypeOf<number | undefined>();
});

test("pipe transform output into nullable target (issue #5694)", () => {
  const backEnd = z.object({ field: z.number().min(1).max(100).nullable() });
  backEnd.extend({
    field: z.string().nonempty().transform(Number).pipe(backEnd.shape.field),
  });
});

Either way, two more issues fall behind this PR — worth flagging in the description if you'd like.

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.

Branding gets dropped in zod v4 from transforms and pipes

2 participants