zod icon indicating copy to clipboard operation
zod copied to clipboard

Problems with `.transform` and generics

Open JacobWeisenburger opened this issue 2 years ago • 10 comments
trafficstars

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.

JacobWeisenburger avatar Feb 02 '23 16:02 JacobWeisenburger

I have confirmed this is working as stated by @stuartkeith

JacobWeisenburger avatar Feb 02 '23 16:02 JacobWeisenburger

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

JacobWeisenburger avatar Feb 02 '23 17:02 JacobWeisenburger

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. 🤔

donferi avatar Feb 09 '23 19:02 donferi

Also running into this issue - @donferi did you end up finding a workaround?

ghost avatar Mar 01 '23 14:03 ghost

This is largely fixed in zod@canary I think. @skdillon @donferi can you try this and report back?

colinhacks avatar Mar 06 '23 06:03 colinhacks

@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

JacobWeisenburger avatar Mar 06 '23 14:03 JacobWeisenburger

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.

ts playground

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`.

wai-lin avatar May 30 '23 04:05 wai-lin

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.

stale[bot] avatar Aug 29 '23 19:08 stale[bot]

Has this been resolved?

ghost avatar Aug 29 '23 19:08 ghost

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!

stuartkeith avatar May 22 '25 12:05 stuartkeith

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  Join Discord Share on X

dosubot[bot] avatar Jul 22 '25 03:07 dosubot[bot]