Spec problem with generic type inference

Topics: General, Language Specification
Oct 18, 2013 at 10:09 PM
As reported in bug #1805, I encountered an issue with type inference and generic type parameters. Here is a reduced test case from our app:
class Thing {
  // Note: There is no method foo()
}

class Wrapper<T extends Thing> {
  constructor(bar: (thing: T) => void) {}
}

// Expected: The property 'foo' does not exist on value of type 'Thing'.
var baz = new Wrapper(object => object.foo());
The above code was compiled with --noImplicitAny and doesn't cause any errors either on 0.9.1-1 or on develop. I either expect that 1) the call to the non-existent method foo() is caught by the compiler or that 2) the --noImplicitAny flag forces me to provide an annotation for the lambda parameter "object". Option 1 would be preferred but option 2 would also catch this bug. I did not expect the lambda parameter object to implicitly have the type any, especially with the --noImplicitAny flag.

Apparently this is by design, according to section 3.8.5 in the new spec. Type parameter constraints are ignored during type inference. Is this a fundamental limitation of the type inference algorithm, or would it be possible to implement? If so, why is it more useful the way it is now than with better type inference? If not, why is the any type implicitly inserted despite the --noImplicitAny flag?
Developer
Oct 18, 2013 at 11:31 PM
I'm pretty sure this is a bug, you can reopen the issue. The generic constraint is not considered as a candidate type during type argument inference but the algorithm is not designed to infer 'any' when a proper candidate cannot be found. In addition, at the moment --noImplicitAny does not warn when 'any' is inferred for a generic type argument, we may expand its capabilities to do that at some later date.

More specifically, during the process of inferring a type for T when constructing Wrapper, the compiler stops producing candidates for T upon finding that T is used as an argument type in a function typed argument (in the spec this is described as saying T becomes fixed). At that point the compiler will then choose a concrete type to use for T for the rest of the inference process so that it can effectively process the body of that function type (and any successive function typed arguments). It does so by saying that T is now considered to be the widened form of the best common type of the current set of candidates for T. Since there are no candidates for T at this point the best common type is {} (empty object) whose widened form is {}. So object should be of type {} and object.foo would report an error. If the constructor for Wrapper were instead defined as:
constructor(x: T, bar: (thing: T) => void) {}
and you called it like:
var baz = new Wrapper("hello", object => object.foo());
then type argument inference would generate 'string' as a candidate for T while processing the first argument, then it would process the second argument and fix T. So T would be considered 'string' the rest of the way and you would see an error like 'the property 'foo' does not exist on value of type "string"' inside the lambda body.