[V4] ZodSafeParseResult only keeps the output type of the schema
When using safeParse, the returned ZodSafeParseResult only keeps track of the output type of the schema. As a result, when trying to format errors with treeifyError, the resulting ZodErrorTree uses the type of the schema output instead of the input. This leads to incorrect typing when piping a schema to a transform:
import { z, treeifyError } from 'zod';
type A = { a: number };
type B = { b: string };
const baseSchema = z.object({
a: z.number().max(9),
});
const transform = z.transform<A, B>(({ a }) => ({
b: a.toString(),
}));
const pipe = baseSchema.pipe(transform);
const errors = pipe.safeParse({ a: 10 })?.error;
if (errors) {
console.log(treeifyError(errors).properties?.a); // Typescript error but the property exists
console.log(treeifyError(errors).properties?.b); // No Typescript error but the property does not exist
}
Yeah, it's a known unsoundness. There's nothing Zod can really do here. z.treeifyError is a convenience method, and it works in the 98% case. You can do get the generic like so to prevent Zod from assuming the output type:
z.treeifyError<unknown>(errors)
You can also pass in a different type entirely (the input type, etc) but you'll have to do some casting on errors.
it works in the 98% case
The docs for .transform says that "Piping some schema into a transform is a common pattern", and the bug occurs any times this is done, so is it that rare ?
There's nothing Zod can really do here.
This used to work in Zod v3 because SafeParseReturnType kept both the type of the input and the output. Is it no longer possible with the v4 architecture to have for example ZodSafeParseResult<Input, Output> ?
I have the same issue. This is a bug in the ZodSafeParseResult type.
export type ZodSafeParseResult<T> = ZodSafeParseSuccess<T> | ZodSafeParseError<T>;
We should have separate input and output types in the safe parse result.
export type ZodSafeParseResult<O, I> = ZodSafeParseSuccess<O> | ZodSafeParseError<I>;
May we please get this fixed?