Sebastian Münster
Sebastian Münster

Reputation: 575

Compiler unable to resolve Generic if not explicitly set

I have the following, simple code:

class Model {
    prop1: number;
}

class A<TModel> {
    constructor(p: (model: TModel) => any) {}
    bar = (): A<TModel> => {
        return this;
    }
}

function foo<T>(p: A<Model>) { }

Expample 1:

foo(new A(x => x.prop1)) // Works

Expample 2:

foo(new A<Model>(x => x.prop1).bar()) // Works

Expample 3:

foo(new A(x => x.prop1).bar()) // Doesn't work. (Property 'prop1' does not exist on type '{}'.)

My "problem" is now that I want example 3 work exactly like example 2. But the Ts compiler seems to be unable to "keep" the generic type, if it isn't explicitly set and the method "bar" is immediately called after the constructor. My question now. Is this a bug or am I just doing something wrong and if so, how can I solve this?

Please let me know if some information is missing.

Upvotes: 0

Views: 87

Answers (1)

jcalz
jcalz

Reputation: 329248

Much of the type inference in TypeScript is done in a "forward in time" direction. That is, it infers the types of an expression given the types of its pieces. For example,

declare const x: string;
declare function f<T>(x: T): Array<T>;
const y = f(x); // y inferred as Array<string>;

In this case the type of f(x) is determined by the type of x and the signature of f. This is relatively straightforward for the compiler to do, because it more or less simulates the normal operation of a JavaScript runtime, which knows f and x and computes f(x).


But there's also contextual typing, in which inference happens in sort of a "backwards in time" direction. That is, the compiler knows the expected type of an expression and the types of some but not all of its pieces, and tries to infer what the type of the missing piece or pieces must have been to produce the expected result. For example:

declare function anyX<T>(): T;
const someX = anyX(); // inferred as {}
declare function f<T>(x: T): Array<T>;
const y: Array<string> = f(anyX()); // anyX() inferred as string

In this case, the function anyX() is generic, but there is no way to infer T from the parameters to anyX() (since it takes no parameters). And, if you just call it directly (as in someX), it will fail to infer and become the empty type {}.

But the compiler knows that y is supposed to be an Array<string>, and that f() will take the type of its input and return an array of that type. So it is able to infer that anyX() must return a string in that case.

Your "Example 1" code is an instance of contextual typing which the compiler does perform: inference of function (or constructor) parameter types from its return (or instance) type. And it works for nested function/constructor calls too:

const z: Array<Array<Array<string>>> = f(f(f(anyX())));

Now, contextual typing is great, but it doesn't happen in all conceivable situations. And apparently one place it does not occur is in inferring the generic type parameter of a generic type/interface/class given the types of its properties or methods:

type G<T> = { g: T; }
declare function anyG<T>(): G<T>;
const h: string = anyG().g; // error! no contextual typing here

This is the problem with your "Example 3". The compiler does not delay inferring the return type of anyG() until after the type of g is consulted. Instead the compiler aggressively computes the return type, and, failing to infer T from anything, makes it G<{}>. And there's an error.

I don't know if there's a perfect answer as to why no contextual typing happens here. Likely it's because it would require a lot more processing time to perform and is usually not worth it (since this use case doesn't happen all the time). I searched TypeScript's GitHub issues but didn't find anything appropriate. If you think your use case is compelling you could file an issue there (after doing a more thorough search so as not to duplicate an existing issue), but I wouldn't hold my breath waiting for someone to address it.

Instead, the workaround like your "Example 2" of manually specifying the uninferred type parameter is a reasonable way forward:

const h: string = anyG<string>().g; // okay

Or, if this keeps happening to you and you'd like some more type safety, you can wrap the trouble code in a function and take advantage of contextual type inference from return type to parameter type, which we know works:

class Model {
  prop1!: number;
}

class A<TModel> {
  constructor(p: (model: TModel) => any) {}
  bar = (): A<TModel> => {
    return this;
  };
}

function foo(p: A<Model>) {}

// helper function to get some contextual typing
function fixIt<TModel>(p: (model: TModel) => any): A<TModel> {
  return new A(p).bar();
}

foo(fixIt(x => x.prop1)); // okay now

Okay, that's as close as I can get to an answer. Hope that helps; good luck!

Upvotes: 2

Related Questions