Type Argument Inference of Function Calls should not depend on the argument order

Topics: Language Specification
Dec 3, 2013 at 5:07 AM
Edited Dec 3, 2013 at 9:31 AM
I've found strange behavior of type inference of function call and filed as #1960.
Issue was closed with explanation that it follows the specification.

But it looks as bug for me. I think it should not depend on the order of arguments.

Specification says [4.12.2 Type Argument Inference]: "Proceeding from left to right, each argument expression e is inferentially typed by its corresponding parameter type P, possibly causing some inferred type arguments to become fixed, and candidate type inferences (section 3.8.6) are made for unfixed inferred type arguments from the type computed for e to P."

I propose to separate this processing into phases (on each phase arguments still be processing from left to right). And the phase of fixing types referred by argument which is anonymous function call will be after the phase of contextually typing arguments.
In this case, more specific types referred by argument will be fixed earlier even if it appear later in the argument list.
Dec 3, 2013 at 2:01 PM
I have argued elsewhere that {} is not a good default type (neither for overload resolution, not for generics instantiation).
Combining that with directional inference results in type-level coercion effects, where I would expect type propagation and type constraint checking.

Consider
function caller<T>(cb:(t:T)=>T,t:T) { return cb(t) }

caller(t=>t,1)+2; // 1 coerced to {}

caller(t=>t,undefined).toString(); // undefined coerced to {}

Object.keys( caller(t=>t,1) ); // 1 coerced to {}
We get a static type error only for the first call, which is the only one that doesn't lead to a runtime type error. Almost a worst-case scenario for a type system.

If t=>t ensured that in and out type are identical, number and undefined would not only have to be assignable to {}, but vice versa as well.
Dec 3, 2013 at 2:14 PM
And new one reason. I know the type system of TypeScript and C# is different, but lot of developers come from .Net C# world. And in C# type inference have another behavior, and I, as a C# developer, expect same from TypeScript.
Coordinator
Dec 3, 2013 at 9:08 PM
Perhaps Anders will jump in here, as he probably will remember better than I do.

The type inference algorithm we chose is a compromise between getting local inference and trying to type check quickly. To avoid doing things like multiple passes, inferring to a fixpoint, or unification, we picked a set of rules that would give good (but not perfect) inference in a single pass. We knew that there would be cases (right to left instead of left to right, for example) where the inference wouldn't bind to types in an order that could be solved in one pass.

That said, I agree that it's tempting to think of multi-pass algorithms (eg, looking at * before ->), which would let you move the dial enough to get some more inference. That would probably an additional cost to type-checking. I would be interested, if someone did decide to explore this, what the added cost was for the additional checking, and what additional examples work for that added cost. Lots of exploration here that's possible.
Dec 7, 2013 at 12:00 PM
I've nothing additional to add to what has already been said but I just wanted to voice my support of this being investigated.
Dec 9, 2013 at 1:59 PM
I dont mind too much the reduction in the type inference, especially if it speeds up the type checking.

The problem I see with it is the change to defaulting to {} when it cant infer a type parameter, which can cause compiler errors. If it defaulted to any the code could still compile, perhaps there could be a compiler warning when it cant infer a generic type so users could explicitly set it if they wanted to.

At the moment I am considering stripping out the generics from some of the definition files I have done and just replacing those parameters with any. That way the definition is usable without having to add in a lot of typescript specific syntax.

Forcing users to explicitly state generic parameters just raises the barrier to using typescript. Typing should be optional not a requirement.
Coordinator
Dec 10, 2013 at 4:07 PM
@nestalk - We ended up switching to {} instead of 'any' for when a type couldn't be inferred because it's like saying "we don't have any information at all about this type". It's the same thing we default to inside of a generic function when you don't tell the compiler anything about the constraints on the generic type:
function f<T>(x?:T) {
  x.test();  // error because x has shape {}, as we don't know anything about it
  return x;
}

var y = f();  // y has shape {} for the same reason, we have nothing that tells us about the generic type T
Using generics is like a way to work with types more directly, and it has a sense of being more about stricter typing rather than looser typing. If we inferred 'any' instead, 1) we're arguably less consistent, looking at the example and 2) we get into situations where you aren't being type-checked because you missed one thing. This comes up with the "best common type" algorithm, which is used quite a lot in our type system.

For example, "best common type" is used to figure out T in the example below:
function f<T>(x:T, y:T): T {
  return x;
}

var y = f(2, {x:2})
var z = y.x;  // now this errors, catching an issue that had snuck in
We could have kept the 'any' and made the type system a bit looser, and there definitely advantages to being loose. That said, we're hearing back that the tightening we've been doing has also been helping people find real bugs in their code.
Dec 10, 2013 at 6:48 PM
jonturner wrote:
@nestalk - We ended up switching to {} instead of 'any' for when a type couldn't be inferred because it's like saying "we don't have any information at all about this type". It's the same thing we default to inside of a generic function when you don't tell the compiler anything about the constraints on the generic type:
But what you implemented does not match your declared intention - it is a hack that does not quite work (cf my examples above):
  • {} is an object type
  • not everything in JS is an object, not every JS context coerces primitives into objects
  • {} does not represent an inference failure, but is a valid type in many JS contexts, so the wrong type bubbles through further inferences, raising errors that are not obvious to trace back to their cause
Using any is slightly wrong, because while it means "no info", it is used for "you probably know what you're doing", not for "you probably missed something here". But this can be remedied by flagging all implicit uses of any whereas there is no such option for {}.

Using {} is more wrong than using any, even though it highlights more errors, because it hides inconsistencies without any explicit flag that type inference has failed.

Using void might work better than {}, as it assumes no object-ness.

None of this addresses the directionality hack - proper unification does not need to be slow, fixpoints might be slow, but only for cases which the current type system cannot handle at all.
Coordinator
Dec 10, 2013 at 10:33 PM
I agree that picking {} was more consistent, but it too is a compromise. We could have errored at any place where the best common type could not be calculated. Unfortunately, this would have broken quite a bit of existing JavaScript code. One pattern comes from creating an array of JSON objects:
var x = [{a: 3, b: 4}, {a: 3, c: 5}];
As we don't try to fabricate a new type from the options of {a: number; b: number} and {a: number; c: number}, we're left to either error or pick a simple type that represents not converging. One of the goals for TypeScript was, as much as possible, for the type system to not get in the way when you're trying to do common patterns. This isn't always possible, but we occasionally do instead try to "pick a type and keep going" to cut down on the amount of noise the type system generates. This is what we opted to do in this situation.

I agree this has some drawbacks. Like you mention, this will sometimes error later on, forcing the user to backtrack. The idea is to help focus on bugs that come from how you use what you provide rather than saying what you've done is wrong from the outset. This helps porting existing JavaScript, and is definitely a trade-off.
Dec 12, 2013 at 6:30 AM
Edited Dec 12, 2013 at 6:35 AM
I agree with Igorbek, clausreinke and nestalk.

After having seen the example given by clausreinke, I wonder if I should use Typescript at all.
I do not want to deal with more weird stuff than Javascript already offers.
Dec 13, 2013 at 9:20 AM
Just to clarify again. Simple example:
function f<T>(func: (value: T) => T, T value): void;
f(value => value + 1, 100); // error
We have all the information to infer T is number by second argument. TS 0.9.5 will infer T is {}.
When we change order of parameters:
function f<T>(T value, func: (value: T) => T): void;
f(100, value => value + 1); // ok
Type was inferred right with same information.
Dec 13, 2013 at 4:04 PM
Edited Dec 13, 2013 at 4:08 PM
@aa2, forgive me but you're being a bit silly to say that you don't want to use TypeScript at all.

The "weird stuff" that TypeScript throws up (and I can only think of two cases, this one and this one ) is the result of attempting to type a completely dynamic language. For both these cases the decision has been made on the basis of practicality rather than theoretical mumbo-jumbo. Practicality to me means identifying the most number of errors at compile time; and this is sometimes rooted on a best guess of intention rather than mathematical correctness.
Dec 14, 2013 at 1:13 PM
nabog wrote:
@aa2, forgive me but you're being a bit silly to say that you don't want to use TypeScript at all.

The "weird stuff" that TypeScript throws up (and I can only think of two cases, this one and this one ) is the result of attempting to type a completely dynamic language. For both these cases the decision has been made on the basis of practicality rather than theoretical mumbo-jumbo. Practicality to me means identifying the most number of errors at compile time; and this is sometimes rooted on a best guess of intention rather than mathematical correctness.
I prefer no types or the "any" type to weird and annoying behavior.
False positives and false negatives as in the given examples are a complete no go for me.
All the hassle for the programmer and compiler writer for what benefit ?

I am no expert so maybe I do not fully understand the reasons behind the decisions of the Typescript team.
But since experienced people with real world requirements share my concern and even showed them to me, there is something (very) wrong with the current form of Typescript.