zod
zod copied to clipboard
Problems with `.transform` and generics
Discussed in https://github.com/colinhacks/zod/discussions/1972
Originally posted by stuartkeith February 1, 2023
Hi, I'm having trouble using .transform inside a function where Zod is using a generic passed into that function. For example (note this is a contrived example, not my actual use case):
function helper<Type extends string>(type: Type) {
return z
.object({
type: z.literal(type),
foo: z.number(),
})
.transform((obj) => {
return {
// does not work - obj.type does not exist on `obj`
type: obj.type,
// this is fine
fooDoubled: obj.foo * 2,
};
});
}
const Hello = helper("hello");
const Goodbye = helper("goodbye");
The type of obj inside transform is:
obj: { [k_1 in keyof z.objectUtil.addQuestionMarks<{
type: Type;
foo: number;
}>]: z.objectUtil.addQuestionMarks<{
type: Type;
foo: number;
}>[k_1]; }
obj.type is not accessible on the object at all. I can see the expected type: Type generic is there but it appears that the addQuestionMarks type is losing the generic.
This also happens if I use Type extends z.ZodLiteral<string> and pass that in directly.
I'm not sure if this is a known limitation of zod or a bug/missing feature. Is there any way of getting around this? Thanks.
I have confirmed this is working as stated by @stuartkeith
Here is a minimal example:
function fn<T extends z.Primitive> ( foo: T ) {
return z.object( { foo: z.literal( foo ) } )
.transform( obj => obj.foo )
// ^^^
// Property 'foo' does not exist on type
// '{ [k in keyof addQuestionMarks<{ foo: T; }>]: addQuestionMarks<{ foo: T; }>[k]; }'.
}
const schema = fn( 42 )
type input = z.input<typeof schema>
// type input = {
// foo: 42
// }
// works as expected
type output = z.output<typeof schema>
// type output = any
// should be 42
console.log( schema.parse( { foo: 42 } ) ) // 42
// at runtime everything is working as expected
I'm having the exact same issue, I know is labeled as a confirmed bug, but I'm not sure if it's actually solvable. 🤔
Also running into this issue - @donferi did you end up finding a workaround?
This is largely fixed in zod@canary I think. @skdillon @donferi can you try this and report back?
@colinhacks
Still a problem in zod@canary
import { z } from 'npm:zod@canary'
function fn<T extends z.Primitive> ( foo: T ) {
return z.object( { foo: z.literal( foo ) } )
.transform( obj => obj.foo )
// ^^^
// Property 'foo' does not exist on type
// '{ [k in keyof addQuestionMarks<{ foo: T; }>]: addQuestionMarks<{ foo: T; }>[k]; }'.
}
const schema = fn( 42 )
type input = z.input<typeof schema>
// type input = {
// foo: 42
// }
// works as expected
type output = z.output<typeof schema>
// type output = any
// should be 42
console.log( schema.parse( { foo: 42 } ) ) // 42
// at runtime everything is working as expected
I think I also have the same issues with this. Here's is small reproduction of my use-case. As soon as I added transform, it starts to complain about incorrect type.
import { z } from 'zod';
type InferInputAsCtx<TInput extends object = object> = {
input: z.ZodSchema<TInput>
resolverFn: (ctx: { input: TInput }) => any
}
function resolverGenerator<TInput extends object = object>(options: InferInputAsCtx<TInput>) {
return options
}
const resolver = resolverGenerator({
input: z.object({
// ^? Error Here
name: z.string(),
age: z.string().regex(/^\d*$/).transform(v => Number(v)),
}),
resolverFn(ctx) {
return ctx.input
}
})
console.log(resolver)
This is the error I got.
Type
ZodObject<{
name: ZodString;
age: ZodEffects<ZodString, number, string>;
},
"strip",
ZodTypeAny,
{
name: string;
age: number;
}, {
name: string;
age: string;
}>
is not assignable to type
ZodType<{
name: string;
age: number;
},
ZodTypeDef,
{
name: string;
age: number;
}>
The types of `_input.age` are incompatible between these types.
Type `string` is not assignable to type `number`.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
Has this been resolved?
Hi, I created the original issue (#1972). I saw zod 4 was released recently and the release notes mentioned an overhaul of the types, and that made me think of this problem. It turns out zod 4 handles the generic as expected, there's no issue now with the snippet I posted above.
Thanks for fixing!
This is a known TypeScript inference issue when using .transform with generics in Zod, and it still affects Zod 4/canary. The root cause is that TypeScript widens the type when generics are involved, so property-specific types like obj.foo become inaccessible inside the transform function. This isn't a bug in Zod's runtime, but a tradeoff in how TypeScript handles generics and mapped types in complex schemas (details).
A reliable workaround is to provide an explicit type annotation for your schema's output, or use ReturnType<typeof ...> to help TypeScript resolve the types correctly. For example:
const schema = fn(42);
type Output = ReturnType<typeof schema.parse>; // should be 42
If you need more context or want to track the discussion, see this issue and the Zod 4 changelog. If this answers your question, feel free to close the issue!
To reply, just mention @dosu.
How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other