Search code examples
typescriptzod

Issue with Inferring Type of Input Parameter in Generic Function


I'm encountering a problem with Typescript regarding the inference of types in a generic function.

Here's a simplified version of the code:

type FnObj<I extends z.ZodType> = {
    input: I,
    func: (input: I extends z.ZodType<infer U> ? U : never) => void
}

type Objs<I extends z.ZodType = z.ZodType> = {
    [key: string]: FnObj<I>
}

function createFormDef<FF extends Objs>(formObject: FF) {
    return formObject;
}

createFormDef({
    test: {
        input: z.object({
            d: z.number(),
        }),
        func: (input) => {
            input.dsf // This line is not throwing an error, although it should be typed as {d: number}.
        },
    },
});

I've tried remove generic of Objs to make the single key its own type. But it didn't work. as follows: (also removed the zod part)

type FnObj<I extends any = any> = {
    input: I,
    func: (input: I) => void
}

type Objs = {
    [key: string]: FnObj
}

function createFormDef(formObject: Objs) {
    return formObject;
}

createFormDef({
    test: {
        input: {
            hello: 10
        },
        func: (input) => {
            input.dsf // still not working, should typed as {hello:number}
        },
    },
});

Solution

  • TypeScript doesn't support existentially quantified generics, so you can't say "an object whose property values are FnObj<I> for some I I don't care about". Writing FnObj<any> (which is what your FnObj becomes with your default type argument) doesn't accomplish that, because the any type is not safe, and so FnObj<any> allows things where input and func are completely unrelated.

    Instead of trying to write such a type, it would be better to say "an object whose property values at key K are FnObj<T[K]> for a T that I specify". That is, you would map over an object type T to get a corresponding Objs<T>:

    type Objs<T extends object> = { [K in keyof T]: FnObj<T[K]> }
    
    function createFormDef<T extends object>(formObject: Objs<T>) {
      return formObject;
    }
    

    Now the compiler can infer T from the formObject input:

    const x = createFormDef({
      test: {
        input: {
          hello: 10
        },
        func: (input) => {
          input.dsf // error
        },
      }
    });
    
    x.test.input
    //     ^?(property) input: { hello: number; }
    

    Here T is inferred as {test: {hello: number}}, and thus x is Objs<T> which is {test: FnObj<{hello: number}>}, and thus the compiler can contextually type the input parameter to the fn callback as {hello: number}.

    Playground link to code