-
Notifications
You must be signed in to change notification settings - Fork 12.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Type Inference on function arguments #15114
Comments
Typescript never infers function arguments. So, unless you specify them, they are always going to be |
Right, but is there a reason for them not to? It feels within reach, and not much different than other kinds of inference |
It's very different and very complex. When you consider that any of those calls might have union types or overloaded signatures in their parameter types, it's not even clear what should happen for a single call (let alone multiple calls): declare function f(n: number, s: string): void;
declare function f(s: string, n: number): void;
declare function f(x: boolean, y: boolean): void;
// g(a: ??, b: ??)
function g(a, b) {
f(a, b);
} Current inference is very straightforward: types almost always come from initializers or contextual types. Both of those are static one-pass things that you can follow back as a human. Trying to do inference from call sites looks neat in simple examples but can make it very hard to trace errors to their actual sources, and is unscalable to large programs. |
@RyanCavanaugh Is there some sort of design doc for how TypeScript's type inferer works, and what kind of system it's based on? Eg. I believe a Hindley-Milner inferer would give |
That type would be incorrect because it allows The TypeScript spec https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md defines how inference works at each declaration site. There is no global inference algorithm to describe because TS doesn't do global inference ala H-M. |
I see. So we would have to infer the same signature as f() itself. What was the rationale for not using global inference? Is it that TS tries to do the best it can in the absence of a sound type system? |
Soundness is mostly orthogonal. I think the general problem is that global type inference in an imperative language is much more expensive than in a functional language. The difference between OCaml and Scala is instructive here. There's a reason Flow limits its "global" inference to file boundaries - it's not practical to scale up global inference to an entire large JS program. Global inference also doesn't detect a lot of errors in imperative programs. Consider this code, which is a totally legal JavaScript program: var x = { kind: 'a' };
if (Math.random() > 0.5) {
x = { knd: 'b' };
}
x.type = "foo"; Flow says this program has one error, TypeScript says this program has two errors, H-M I believe says this program has zero errors ( Global inference is also bad at producing sane errors function doSomething(x) {
console.log(x.y);
}
var arr = [{x: 100, y: 100}, {x: 200, z: 200}];
doSomething(arr.pop()); Flow helpfully says
which does not help you at all at figuring out the typo elsewhere in the program. So basically any sane programmer is going to put type annotations on parameters anyway (since you must at module boundaries anyway, and you must if you want sane errors). Once you have type annotations on type parameters, local inference is sufficient for the vast majority of cases. |
@RyanCavanaugh As always, thank you for the detailed and well thought out responses.
This seems to be a philosophical question with tradeoffs. 1-2 errors is restrictive because it requires me to explicitly declare a type with 2 optional members before my program will compile, but as you said, the benefit is errors are local, which makes debugging fast and pleasant. 0 errors is more correct (the union type should be handled downstream, and if it's not, the compiler can warn about an unexhaustive match or possibly missing member), but the drawback is nonlocal errors when you're wondering "why is the compiler saying this a union type?".
How do Haskell, OCaml, and Elm engineers manage to use their languages at scale? From this SO post, it sounds like those languages are more restrictive than JS in what they can express (eg. no class extensions), which makes H-M usable. |
@RyanCavanaugh relative to your initial example, where you call I feel like in that case it would be reasonable to have this generate 3 different types for TypeScript already has the control flow analysis in place to keep track of when For full context: I really want to use Is it not possible to do local inference based on the function body (that is to say: ignoring the function's calling context)? Bringing back the flow example (which I agree is weird):
I feel like the "proper" set of errors that should happen would be:
I realize "well let's just have giant types" feels very hand-wavy... I think a reasonable alternative would be "try to deduce argument types from usage within the function, and opt for Purescript (and its record types) has some functionality close to this, so inference works pretty well (though you tend to lose type aliases. This is already the case in TypeScript though) |
I fully agree with this. The reasoning is very straightforward. It is also worth noting that no type annotations need be placed on the parameters declared by function literals passed as callback or assigned to an already typed local. I am glad that you referenced Scala because it also has essentially the same requirements for where type annotations must be placed as TypeScript does under You also mention OCaml which while it has much stronger type inference than TypeScript or Scala, exhibits some very counterintuitive behavior with respect to inference of function parameter types let square x = x * x
let i = square 4
(* 16 *)
let f = square 4.0
(* Error: This expression has type float but an expression was expected of type int *) |
@aluanhaddad Nit: your example is an Ocamlism, and is not a statement about H-M in general. Eg. Haskell infers let square x = x * x
let i = square 4 -- 16
let f = square 4.0 -- 16.0 |
@bcherny well, this works in Haskell because of type classes, but they are not a part of H-M |
This is a duplicate of #15196, related discussion can be found in #1265, #15114 and #11440. Please see my response in #15196 (comment) |
@mhegazy I don't want to litigate this too much, but what I'm asking for (using the function definition itself to infer parameter types) doesn't seem like non-local type inference? Since the function body is well scoped and doesn't belong to multiple files. I see the argument for not having Haskell-style Maybe I'm missing a detail here or misunderstanding the meaning of "non-local" here. |
@rtpg there's nothing in TypeScript today that infers information about a variable from its usage. I agree it's smaller than Haskell-style inference... but it would be a very large architectural change. And it's still nonlocal since presumably code like this would be expected to produce inferences. function fn(x) {
bar(x);
}
function bar(y) {
baz(y);
}
function baz(z) {
return Math.sqrt(z);
} |
@RyanCavanaugh I thought type guards and flow analysis is doing exactly that - narrowing a type based on usage? Isn't the example above just an extension of that? At least for the simplest cases, it would be nice for TypeScript to achieve parity with Flow. |
@joewood type guards / flow analysis are straightforward (so to speak...) because they're "top-down" - given a statement in a function, it's relatively easy to determine which control paths it's reachable from, because JavaScript doesn't have control flow structures that can go "up" (e.g. COMEFROM). This is very different from a function call - you can be in a function on line 1500 and the first call might be on line 4000. Or maybe you're indirectly called via some callback, etc.. The dream of inference from function calls is really not clear as some people would imply. Here's an example in Flow:
This is "spooky action at a distance" at its most maximal. You can have the call to Realistically there are two cases that usually happen if you use inference from call sites / data flow analysis:
If your file typechecks, cool, no work required. In the other case, the first thing you do to diagnose the problem is... drumroll... add a parameter type annotation so you can figure out where the bad call is coming from. If there's indirection, you'll probably have to do this in layers. You'll end up with a file full of parameter type annotations, which is good since you'll need them anyway for cross-file typechecks. So it's great for a single-file playground demo but for "real" software development, you'll end up with approximately the same number of parameter type annotations as if you used a more local inference algorithm. |
Thanks @RyanCavanaugh for the extensive explanation. I agree, this could become a nightmare of different permutations with contradicting definitions. But, I'm wondering if this is all or nothing? Where a parameter is used definitively inside a function (e.g. against an explicit declared type with no overloads), can a deduction be made about its type? Then, if there's any whiff of ambiguity, including Unions and Overloads, then it backs off to the default Another approach is to make this a tooling feature. An "autofix" feature could be added to fix untyped parameters based on their usage inside the function. This could speed up fixing "no implicit any" migrations. |
That reminds me of Flow's existential types, which is a sort of opt-in H-M prover. I imagine it's a lot of work to build this if it's just some optional feature, though (as opposed to feature everyone would use). The very fact that it's opt-in (while the default type is still |
It's not like H-M in any way
This is not true |
@vkurchatkin Can you explain? This is how I understand the docs I linked. |
@bcherny If you omit type annotation it behaves kind of like |
@RyanCavanaugh There is a simple mechanism for producing sound (but rarely useful) signatures for intersections and unions of functions, described in #14107 (comment). It would work for your example with In reality |
@RyanCavanaugh For the following example:
The ask is for If you only add this feature, and nothing else, how would this give rise to the "spooky action at a distance" problem you are describing? Both |
Similarly, for the following example: function fn(x) {
bar(x);
}
function bar(y) {
baz(y);
}
function baz(z) {
return Math.sqrt(z);
} The inference would be non-local only in the sense that the return type inference is non-local. Just as an inferred return type may affect the inferred return type of a caller, the inferred parameter type of a function may affect the inferred parameter type of a caller. The inference is in unidirectional (barring edge cases like recursion), and in inverse direction to the call graph. To avoid complicated edge cases, you could start with inferring types for parameters that:
It is fine to bail out and infer |
I've made a rudimentary attempt at this in master...masaeedu:master that makes the case in the OP (and some others I am interested in) work. The compiler can build itself, although not all tests pass. The algorithm is as follows. For all parameters that cannot otherwise be typed, an additional step is tried before bailing out and inferring any:
Obviously this is nowhere near complete, and the implementation does not even fit with the codebase style, but it is something concrete to play with. Some simple work to add to this:
Some hard work to add to this:
|
TypeScript Version:
2.3.0
Code
Expected behavior:
I would expect the inferred type of
g
to be(x:any)=>any
, and the infered type ofh
to be(x:number) => number
(due to the restrictions placed by the call off
)When compiling with
noImplicitAny
, I would only expect an error ong
, not onh
Actual behavior:
Running
tsc --declaration
on this snippet gives me:the
x
argument onh
does not seem to use thef(x)
call to tighten the definition.Considering the simplicity of the example, I imagine I might be missing an important detail in the type system that makes this the proper inference result, but I haven't figured it out yet.
The text was updated successfully, but these errors were encountered: