Skip to content

Commit 2514773

Browse files
committed
Fix classic pipe compatibility for branded outputs
1 parent 1373c85 commit 2514773

2 files changed

Lines changed: 37 additions & 2 deletions

File tree

packages/zod/src/v4/classic/schemas.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,10 @@ export interface ZodType<
179179
pipe<T extends core.$ZodType<any, core.output<this>>>(
180180
target: T | core.$ZodType<any, core.output<this>>
181181
): ZodPipe<this, T>;
182+
pipe<const T extends core.SomeType>(
183+
target: T,
184+
...rest: core.output<this> extends core.input<T> ? [] : ["Incompatible pipe target"]
185+
): ZodPipe<this, T>;
182186
readonly(): ZodReadonly<this>;
183187

184188
/** Returns a new instance that has been registered in `z.globalRegistry` with the specified description */
@@ -326,7 +330,7 @@ export const ZodType: core.$constructor<ZodType> = /*@__PURE__*/ core.$construct
326330
return _catch(this, params);
327331
},
328332
pipe(target) {
329-
return pipe(this, target);
333+
return pipe(this, target as core.$ZodType);
330334
},
331335
readonly() {
332336
return readonly(this);

packages/zod/src/v4/classic/tests/pipe.test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect, test } from "vitest";
1+
import { expect, expectTypeOf, test } from "vitest";
22

33
import * as z from "zod/v4";
44

@@ -15,6 +15,37 @@ test("string to number pipe async", async () => {
1515
expect(await schema.parseAsync("1234")).toEqual(1234);
1616
});
1717

18+
test("pipe contextually types transforms", () => {
19+
const schema = z.string().pipe(z.transform((val) => val.toUpperCase()));
20+
expectTypeOf<z.output<typeof schema>>().toEqualTypeOf<string>();
21+
22+
// @ts-expect-error incompatible pipe targets are still rejected
23+
z.string().pipe(z.number());
24+
});
25+
26+
test("pipe branded output to unbranded input", () => {
27+
const zodBrand = z.string().brand<"myBrand">();
28+
const inputSchema = z.object({
29+
a: z.number(),
30+
c: zodBrand,
31+
});
32+
const validateSchema = z.object({
33+
a: z.number(),
34+
c: zodBrand,
35+
});
36+
37+
const testSchemaPipeline = inputSchema.transform((input) => input).pipe(validateSchema);
38+
const testSchemaPipeline2 = inputSchema.pipe(validateSchema);
39+
const testSchemaPipeline3 = inputSchema.pipe(inputSchema);
40+
41+
expectTypeOf<z.output<typeof testSchemaPipeline>>().toEqualTypeOf<{
42+
a: number;
43+
c: string & z.core.$brand<"myBrand">;
44+
}>();
45+
expectTypeOf<z.output<typeof testSchemaPipeline2>>().toEqualTypeOf<z.output<typeof validateSchema>>();
46+
expectTypeOf<z.output<typeof testSchemaPipeline3>>().toEqualTypeOf<z.output<typeof inputSchema>>();
47+
});
48+
1849
test("string with default fallback", () => {
1950
const stringWithDefault = z
2051
.pipe(

0 commit comments

Comments
 (0)