Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 488
prudhvi Avatar asked Nov 26 '19 16:11

prudhvi


People also ask

Can typescript infer the type of a type argument?

Specifying Type Arguments TypeScript can usually infer the intended type arguments in a generic call, but not always. For example, let’s say you wrote a function to combine two arrays: function combine < Type > (arr1: Type [], arr2: Type []): Type [] {

How do I use generics in typescript?

In TypeScript, generics are used when we want to describe a correspondence between two values. We do this by declaring a type parameter in the function signature: By adding a type parameter Type to this function and using it in two places, we’ve created a link between the input of the function (the array) and the output (the return value).

What does never mean in typescript?

The never type represents values which are never observed. In a return type, this means that the function throws an exception or terminates execution of the program. never also appears when TypeScript determines there’s nothing left in a union. x; // has type 'never'!

How do you call a function in typescript?

They’re also values, and just like other values, TypeScript has many ways to describe how functions can be called. Let’s learn about how to write types that describe functions. The simplest way to describe a function is with a function type expression . These types are syntactically similar to arrow functions: console. log ( s );


1 Answers

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

like image 84
jcalz Avatar answered Sep 23 '22 22:09

jcalz