GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Discussion: Parameter type inference from function body by masaeedu · Pull Request #17715 · microsoft/TypeScript

Conversation

@masaeedu

@masaeedu Asad Saeeduddin (masaeedu) commented Aug 10, 2017 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

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:

// CASE 1
function foo(s) {
    Math.sqrt(s)
}
// => function foo(s: number): void

// CASE 2
declare function swapNumberString(n: string): number;
declare function swapNumberString(n: number): string;

function subs(s) {
  return swapNumberString(s);
}
// => function subs(s: string): number

// NOTE: Still broken, needs to deal with overloads. Should have been inferred as:
// => (s: string) => number & (s: number) => string

// CASE 3
function f(x: number){
   return x;
}

function g(x){ return f(x); };
// => function g(x: number): number

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Asad Saeeduddin (masaeedu) added 2 commits August 10, 2017 01:18
@masaeedu
Rudimentary attempt at parameter inference
a602e1f
@masaeedu
Add additional test case
99b5362
@msftclas Microsoft Contribution License Agreements (msftclas) added the cla-already-signed label Aug 10, 2017
@masaeedu Asad Saeeduddin (masaeedu) changed the title DISCUSSION: Parameter type inference from function body Discussion: Parameter type inference from function body Aug 10, 2017
Asad Saeeduddin (masaeedu) added 3 commits August 10, 2017 02:06
@masaeedu
Don't infer anything when no usages found
adfd904
@masaeedu
Don't perform inference for non-parameter declarations
4d8a8b3
@masaeedu
Bail if can't determine symbol for invoked function
d14c511
masaeedu
Asad Saeeduddin (masaeedu) commented Aug 10, 2017
View reviewed changes
Comment thread 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)

@masaeedu Asad Saeeduddin (masaeedu) Aug 10, 2017

Copy link
Copy Markdown
Contributor Author

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.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Asad Saeeduddin (masaeedu) added 2 commits August 10, 2017 02:58
@masaeedu
Bail when invocation symbol's value declaration isn't a function decl…
db39b43
…aration
@masaeedu
Bail on rest parameters
6095dfd
@masaeedu
Asad Saeeduddin (masaeedu) force-pushed the master branch from 32a901d to 6095dfd Compare August 10, 2017 07:54
Asad Saeeduddin (masaeedu) added 5 commits August 10, 2017 03:56
@masaeedu
Formatting, remove unnecessary declarations
5bc6895
@masaeedu
Update testcases
9bec8c1
@masaeedu
Loosen restriction on declarations to 'function-like'
9905af7
@masaeedu
Accept baselines for new test
a5d6542
@masaeedu
Accept change in test due to improved inference
7672747
@masaeedu

Asad Saeeduddin (masaeedu) commented Aug 10, 2017

Copy link
Copy Markdown
Contributor Author

Down to one failed testcase now.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

Asad Saeeduddin (masaeedu) added 2 commits August 10, 2017 04:48
@masaeedu
Bail when argument and parameter lists are misaligned
351f665
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.
@masaeedu
Fix lint
ef5f7bc
@masaeedu

Asad Saeeduddin (masaeedu) commented Aug 10, 2017

Copy link
Copy Markdown
Contributor Author

All tests should be passing now. In terms of implementation details, I still need to figure out how to robustly:

  • Retrieve the parameter symbol associated with a given argument in a CallExpression
  • Obtain a workable signature from things like interface Foo { (bar: Bar): Baz } so I can retrieve the parameters associated with arguments

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.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

HerringtonDarkholme
Herrington Darkholme (HerringtonDarkholme) reviewed Aug 10, 2017
View reviewed changes
Comment thread src/compiler/checker.ts
if (!func.body || isRestParameter(parameter)) return;

const types = checkAndAggregateParameterExpressionTypes(parameter);
return types ? getWidenedType(getIntersectionType(types)) : undefined;

@HerringtonDarkholme Herrington Darkholme (HerringtonDarkholme) Aug 10, 2017

Copy link
Copy Markdown
Contributor

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)
}

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@masaeedu Asad Saeeduddin (masaeedu) Aug 10, 2017

Copy link
Copy Markdown
Contributor Author

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.

Herrington Darkholme (@HerringtonDarkholme) Could you provide a concrete example of this? For which reference is someCond affecting the type?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@masaeedu Asad Saeeduddin (masaeedu) Aug 10, 2017

Copy link
Copy Markdown
Contributor Author

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.

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.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
HerringtonDarkholme
Herrington Darkholme (HerringtonDarkholme) reviewed Aug 10, 2017
View reviewed changes
Comment thread src/compiler/checker.ts
if (!usages.length) return;
const funcSymbol = getSymbolAtLocation(invocation.expression);
if (!funcSymbol || !isFunctionLike(funcSymbol.valueDeclaration)) return;
const sig = getSignatureFromDeclaration(funcSymbol.valueDeclaration);

@HerringtonDarkholme Herrington Darkholme (HerringtonDarkholme) Aug 10, 2017

Copy link
Copy Markdown
Contributor

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.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@masaeedu Asad Saeeduddin (masaeedu) Aug 10, 2017 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor Author

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.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
HerringtonDarkholme
Herrington Darkholme (HerringtonDarkholme) reviewed Aug 10, 2017
View reviewed changes
Comment thread src/compiler/checker.ts
const funcSymbol = getSymbolAtLocation(invocation.expression);
if (!funcSymbol || !isFunctionLike(funcSymbol.valueDeclaration)) return;
const sig = getSignatureFromDeclaration(funcSymbol.valueDeclaration);
const parameterTypes = sig.parameters.map(getTypeOfParameter);

@HerringtonDarkholme Herrington Darkholme (HerringtonDarkholme) Aug 10, 2017

Copy link
Copy Markdown
Contributor

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?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@masaeedu Asad Saeeduddin (masaeedu) Aug 10, 2017

Copy link
Copy Markdown
Contributor Author

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.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
HerringtonDarkholme
Herrington Darkholme (HerringtonDarkholme) reviewed Aug 10, 2017
View reviewed changes
Comment thread src/compiler/checker.ts Outdated
function checkAndAggregateParameterExpressionTypes(parameter: ParameterDeclaration): Type[] {
const func = <FunctionLikeDeclaration>parameter.parent;
const usageTypes: Type[] = [];
forEachInvocation(<Block>func.body, invocation => {

@HerringtonDarkholme Herrington Darkholme (HerringtonDarkholme) Aug 10, 2017

Copy link
Copy Markdown
Contributor

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.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@masaeedu Asad Saeeduddin (masaeedu) Aug 10, 2017

Copy link
Copy Markdown
Contributor Author

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.

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.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
HerringtonDarkholme
Herrington Darkholme (HerringtonDarkholme) reviewed Aug 10, 2017
View reviewed changes

@HerringtonDarkholme Herrington Darkholme (HerringtonDarkholme) left a comment

Copy link
Copy Markdown
Contributor

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

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Asad Saeeduddin (masaeedu) added 2 commits August 10, 2017 06:27
@masaeedu
Recursively scan function arguments
15b6c0c
@masaeedu
Rename
d85a212
@masaeedu

Asad Saeeduddin (masaeedu) commented Aug 10, 2017

Copy link
Copy Markdown
Contributor Author

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 noImplicitAny when a sensible inference cannot be made.

This is useful because when converting a new project to TypeScript, you often end up either disabling noImplicitAny and getting any everywhere, or manually propagating types up the call graph by adding parameter types to every function expression. Some kind of middle-ground would be useful, especially if this allows you to automatically generate explicit types for top-level APIs more accurately.

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.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@masaeedu
Asad Saeeduddin (masaeedu) force-pushed the master branch from 8def2ea to e601cf0 Compare August 10, 2017 10:49
@masaeedu
Accept baselines
cd788ee
@masaeedu
Asad Saeeduddin (masaeedu) force-pushed the master branch from e601cf0 to cd788ee Compare August 10, 2017 10:51
Asad Saeeduddin (masaeedu) added 3 commits August 10, 2017 07:08
@masaeedu
Fix lint
f2853ae
@masaeedu
Add comments to describe test cases
aff19fc
@masaeedu
Expand testcases
818b9c9
@msftgits Microsoft GitHub User (msftgits) removed the cla-already-signed label Sep 26, 2017
@mhegazy

Mohamed Hegazy (mhegazy) commented Nov 8, 2017

Copy link
Copy Markdown
Contributor

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 { kind: number } instead of Node, or { length: number } instead of Array which correct.. but not what a human would have thought about.. it gets complicated as the type gets more complex, and then you have these anonymous types that really mean nothing to the developer..

The other problem is this disables checking on the parameter all together. true that an implicit any inference does that as well, but at least you have a way to flag these as errors in --noImplicitAny.

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.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@mhegazy Mohamed Hegazy (mhegazy) closed this Nov 8, 2017
@masaeedu

Asad Saeeduddin (masaeedu) commented Nov 8, 2017 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor Author

Mohamed Hegazy (@mhegazy) The overall goal of this and #17428 is to have the type <T extends { length }>(a: T) => T["length"] ascribed to a => a.length. This type means nothing to the developer when they really meant to talk about Array#length, but this is fine: the inferred type is sufficiently polymorphic that applying it to [1, 2, 3] will correctly produce number, and incorrectly applying it will produce an error.

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.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@mhegazy

Mohamed Hegazy (mhegazy) commented Nov 8, 2017

Copy link
Copy Markdown
Contributor

#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.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@masaeedu

Asad Saeeduddin (masaeedu) commented Nov 8, 2017

Copy link
Copy Markdown
Contributor Author

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 😉

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@mhegazy

Mohamed Hegazy (mhegazy) commented Nov 8, 2017

Copy link
Copy Markdown
Contributor

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 --noImplicitAny, and we have experimented with doing out-of-band inference in #14786. So having a proposal with this in mind would be very helpful.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@masaeedu

Asad Saeeduddin (masaeedu) commented Nov 8, 2017

Copy link
Copy Markdown
Contributor Author

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.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@microsoft Microsoft (microsoft) locked and limited conversation to collaborators Jun 14, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Reviewers

1 more reviewer
@HerringtonDarkholme Herrington Darkholme (HerringtonDarkholme) HerringtonDarkholme left review comments
Reviewers whose approvals may not affect merge requirements

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

5 participants

@masaeedu @mhegazy @HerringtonDarkholme @msftclas @msftgits
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.