-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Discussion: Parameter type inference from function body #17715
Conversation
src/compiler/checker.ts
Outdated
.map((arg, i) => ({ arg, symbol: getSymbolAtLocation(arg), i })) | ||
.filter(({ symbol }) => symbol && symbol.valueDeclaration === parameter) | ||
if (!usages.length) return | ||
const funcSymbol = getSymbolAtLocation(invocation.expression) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to find robust mechanism for resolving the node corresponding to the invoked function to a function signature. Right now things like interface Foo{ (bar: Bar): Baz }
can mess it up.
Down to one failed testcase now. |
The mechanism used to match up arguments and parameters here is very brittle, and can easily be broken by e.g. rest parameters. For purposes of the POC we deal with this by simply discarding such a usage site.
All tests should be passing now. In terms of implementation details, I still need to figure out how to robustly:
That aside, I think it would be good to start looking at examples of tricky cases where either the implementation or the proposed semantics blow up. |
if (!func.body || isRestParameter(parameter)) return; | ||
|
||
const types = checkAndAggregateParameterExpressionTypes(parameter); | ||
return types ? getWidenedType(getIntersectionType(types)) : undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getIntersectionType cannot handle flow sensitive typing. For example,
function test(a) {
if (someCond) roundNumber(a)
else trimString(a)
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@HerringtonDarkholme Could you provide a concrete example of this? For which reference is someCond
affecting the type?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@HerringtonDarkholme I understand what you mean now; in the case you highlighted a
should be inferred as number | string
instead of number & string
. number & string
is an excessively conservative inference, but it is a sound one: number & string
is assignable to number | string
.
Some options are to bail and infer any
for parameters used in branched code, keep the existing behavior and expect the user to manually supply typings when they realize number & string
is unsatisfiable, or to actually analyze the flow graph and generate the appropriate union type.
if (!usages.length) return; | ||
const funcSymbol = getSymbolAtLocation(invocation.expression); | ||
if (!funcSymbol || !isFunctionLike(funcSymbol.valueDeclaration)) return; | ||
const sig = getSignatureFromDeclaration(funcSymbol.valueDeclaration); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does this work with recursive function? I guess it need some guard statement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getTypeOfParameter
calls getTypeOfSymbol
, which internally has a stack checking for circularity and ejecting with "unknown". I believe we can rely on this, but I need to add tests.
const funcSymbol = getSymbolAtLocation(invocation.expression); | ||
if (!funcSymbol || !isFunctionLike(funcSymbol.valueDeclaration)) return; | ||
const sig = getSignatureFromDeclaration(funcSymbol.valueDeclaration); | ||
const parameterTypes = sig.parameters.map(getTypeOfParameter); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if the parameterType is a generic type parameter? Should it be propagated to the inferred function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, probably it should be ensured that the function currently under inference does not already have an explicit type parameter list, and a type parameter to add polymorphism wrt the parameter under inference. If there is already a type parameter list we should just bail.
src/compiler/checker.ts
Outdated
function checkAndAggregateParameterExpressionTypes(parameter: ParameterDeclaration): Type[] { | ||
const func = <FunctionLikeDeclaration>parameter.parent; | ||
const usageTypes: Type[] = []; | ||
forEachInvocation(<Block>func.body, invocation => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clearly, only collecting invocation isn't enough. But I wonder how other expressions can be handled. For example, how arg.assigment = 123
is handled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@HerringtonDarkholme I've put out some initial thoughts in #15114 (comment). We can exploit co and contravariance to get sensible rules here. If something assigns to a parameter, the sensible thing is for the parameter to be inferred as having a supertype of said assignment. Unfortunately TypeScript can't represent this.
This means we should restrict ourselves to inference in covariant usages of the parameter variable, i.e. wherever it is used as a source of information (assignment of the parameter to well-typed reference, invocation of the parameter with well-typed argument list, accessing properties on parameter, etc.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the pull request!
But I feel the quality of parameter inference doesn't match other inference already in TS.
However, it's another problem whether implementing full inference is worthy
@HerringtonDarkholme Thanks for the feedback! This is not intended to be a mergeable implementation of parameter inference; the goal is to demonstrate that it is possible to sensibly infer parameter types recursively without breaking everything in the compiler, and that it's possible to do so incrementally, reverting to This is useful because when converting a new project to TypeScript, you often end up either disabling I covered only invocation expressions here because these seemed easiest to implement for a POC (and mirror the example from #15114), but the same concept extends to assignment from parameter, operator usage, etc. |
I have recently worked on something similar to this in #14786. I started off with inferring from function body, but putting it to usage, changed my mind. most functions do not just use the parameter as is, they usually look at one ore more properties from it.. the result of the inference from the function body is always more of a constraint than a type really.. you end up with things like The other problem is this disables checking on the parameter all together. true that an implicit I think the right place for this work is quick fix/ refactoring to help the user migrate to TS. The bar is low here for accuracy, since the user is doing an explicit action and will be reviewing the result anyways. Feel free to look at #14786, and contribute changes. |
@mhegazy The overall goal of this and #17428 is to have the type Inferring things in this way ensures the function demands no more specificity in its parameters, and promises no less specificity in its return type, than what its implementation dictates. It is true that inferring from the body will defer discovery of any type errors in the function implementation to the point where the function is invoked, but this is a worthwhile tradeoff for the significant decrease in verbosity and the increased ease of converting existing codebases to TypeScript. As an example, if you do inference in this manner, it becomes more feasible for the typechecker to interact directly with plain JS modules that don't have any explicit typings. |
#17428 is a breaking change.. a big one mind you. inferring constraints from the body of functions makes it less so, but still not completely. I would like to see a full proposal for this if you have one. |
@mhegazy I'm going to be on vacation in a bit, and will spend some time to see if can come up with a proposal/reference implementation. That way you'll have a concrete list of thousands of broken testcases to point to when you reject the proposal 😉 |
This is not the intention of my earlier comment.. I think if we are going to consider a major change, we should have the full picture, rather than having it spread on multiple smaller issues. In general this has a better chance of a positive outcome. It is also important to acknowledge the proposal in #17428 is a breaking change, and a viable proposal needs to consider how it can be added/phased in with limited disruption to the system. Assuming we can make a fundamental change of behavior like this is with no mitigation is not realistic.. we have experience with similar changes with limiting any type evolution to under |
@mhegazy I totally agree with you regarding the necessity of a proper proposal, my last comment was in jest. Exactly how much of a breaking change it would be, and whether it is possible to come up with a compromise that doesn't break 90% of existing TypeScript code is something I'm interested in myself. |
This PR is not intended to be merged as-is, it is an attempt at demonstrating the feasibility of #15114, and is a request for revisiting this topic.
The change facilitates inference of parameter types from usage as arguments within the function body. Some motivating examples and the inferred types are shown below: