prudhvi
prudhvi

Reputation: 138

Typescript generics: infer type from the type of function arguments?

I have a method that has 2 arguments and I want it to infer a type from the 1st argument.

For example, in the following code, I want the type T of the function create_C<T> to be inferred from the firstArgument so that the return type of create_C function would be C<type inferred from firstArgument>

interface C<T> { 
    firstArgument: A<T>;
    secondArgument: (obj: any) => T
}

export interface A<T> {
    type: T;
}

function create_C<T>(
    firstArgument: A<T>,
    secondArgument: (obj: any) => T
): C<T> {
    return {
        firstArgument,
        secondArgument
    }
}

However, in the following implementation, the type of const c is being inferred as C<{ prop2: number }>. But I am expecting it to be inferred as C<B> and I am expecting the compiler to throw an error saying that the return type of the secondArgument is not of type B

interface B { 
    prop1: string;
    prop2: number
}

export class B_Component implements A<B> {
    type: B = {
        prop1: "",
        prop2: 1
    };
}

const c = create_C(
    new B_Component(),
    () => ({ prop2: 2 })
)

How can I make sure for the compiler to throw an error saying that the return type of the secondArgument is not of type B?

Here is a Stackblitz editor link: https://stackblitz.com/edit/qqddsn

Upvotes: 9

Views: 6258

Answers (2)

jcalz
jcalz

Reputation: 327624

In your function signature

declare function create_C<T>(a1: A<T>, a2: (obj: any) => T): C<T>;

there are two inference sites for T (an "inference site" means "someplace the compiler can use to try to infer a type for a type parameter"). One site is from the type property of the first argument a1, and the other site is the return type of the second argument a2. The compiler looks at a call like

create_C(new B_Component(), () => ({ prop2: 2 });

and tries to infer T from both sites. In this case, there is a match: both (new B_Component()).type and {prop2: 2} are assignable to {prop2: number}. So there's no error, and you get C<{prop2: number> coming out. In another situation, this might be exactly the behavior you want from the compiler.


Instead, you want to see the compiler use just a1 to infer T, and to just verify that a2 matches it. That is, you want the T in (obj: any) => T to be a non-inferential type parameter (see microsoft/TypeScript#14829). Unfortunately, there is no "official" support for this. But fortunately, there are workaround techniques which can often be used to get this behavior.

Here's one such technique: if you change a type parameter in an inference site from T to T & {}, it lowers the site's priority. So the compiler will tend to infer T from other inference sites first and only come back to the T & {} one if it fails to infer from other places. And the type T & {} is very similar to T (if T is an object type then it's basically the same) so it doesn't change the semantics much. Let's try it:

declare function create_C_better<T>(a: A<T>, b: (obj: any) => T & {}): C<T>;

Here goes:

const c2 = create_C_better(
    new B_Component(),
    () => ({ prop2: 2 }) // error!
    //    ~~~~~~~~~~~~~~ <-- prop1 is missing
)

const c3 = create_C_better(
    new B_Component(),
    () => ({ prop1: "all right", prop2: 2 })
); // C<B>

There, you get the error you wanted when prop1 is missing, and when you fix it, you get an output of type C<B> as desired.


Okay, hope that helps; good luck!

Link to code

Upvotes: 12

Nidin Vinayakan
Nidin Vinayakan

Reputation: 1227

It is due to the secondArgument: (obj: any) => T. If you apply above definition to () => ({ prop2: 2 }) Type of T is { prop2: number }. You may change it to something else to get the desired result. For example.

    interface C<T> {
      firstArgument: A<T>;
      secondArgument: (obj: any) => any;
    }

    export interface A<T> {
      type: T;
    }

    declare function create_C<T>(
      firstArgument: A<T>,
      secondArgument: (obj: any) => any
    ): C<T>;

    interface B {
      prop1: string;
      prop2: number;
    }

    export class B_Component implements A<B> {
      type: B;
      configuration: B = {
        prop1: "",
        prop2: 1
      };
    }
    const b = new B_Component();
    export const c = create_C(b, () => ({ prop2: 2 }));

Upvotes: -1

Related Questions