GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — Discussion: Parameter type inference from function body by masaeedu · Pull Request #17715 · microsoft/TypeScript Conversation
|
| .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.
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.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
32a901d to
6095dfd
Compare
|
Down to one failed testcase now. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
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. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| if (!func.body || isRestParameter(parameter)) return; | ||
|
|
||
| const types = checkAndAggregateParameterExpressionTypes(parameter); | ||
| return types ? getWidenedType(getIntersectionType(types)) : undefined; |
There was a problem 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) }
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Herrington Darkholme (@HerringtonDarkholme) Could you provide a concrete example of this? For which reference is someCond affecting the type?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Herrington Darkholme (@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.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| 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.
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.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was a problem 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.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| 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.
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?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem 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.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| 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.
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.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Herrington Darkholme (@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.)
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem 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
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Herrington Darkholme (@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. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
8def2ea to
e601cf0
Compare
e601cf0 to
cd788ee
Compare
|
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. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
Mohamed Hegazy (@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. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
#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. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Mohamed Hegazy (@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 😉 |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
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 |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Mohamed Hegazy (@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. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
There was an error while loading. Please reload this page.