Skip to content
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

Typescript does not implicitly infers types when providing function as arguments? #17520

Closed
1ven opened this issue Jul 30, 2017 · 5 comments
Closed
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@1ven
Copy link

1ven commented Jul 30, 2017

When providing identity function to map, it's return type is incorrect(see x2), while providing anonymous function as a callback leads to a correct output type(see x1).

TypeScript Version: 2.4.0 / nightly (2.5.0-dev.201xxxxx)

Code

const map = <T, U>(f: (x: T) => U, arr: T[]): U[] => {
  return arr.map((val) => f(val));
}

// x1 type is number[]
const x1 = map(x => x, [1, 2, 3]);

const identity = <T>(x: T) => x
// x2 type is {}[]
const x2 = map(identity, [1, 2, 3]);

Expected behavior:
Return type of map function, when providing identity function should be number[]

Actual behavior:
Return type is {}[]


Also, I've realized that map function in 2nd example does not infers even T type. It's {} currently, while it's obvious should be a number type, as we are providing array of numbers:
screen shot 2017-07-30 at 11 47 52 pm

The second interesting thing is, that type inferring works fine, if map function arguments will be reversed:

const map = <T, U>(arr: T[], f: (x: T) => U): U[] => {
  return arr.map((val) => f(val));
}
// x2 type is number[]
const x2 = map1([1, 2, 3], identity);
@ahejlsberg
Copy link
Member

This is an effect of TypeScript's type argument inference algorithm:

  • First, we process (from left to right) all arguments that are deemed context insensitive, which effectively means all arguments that don't contain function expressions with un-annotated parameters.
  • Then, we then separately process the context sensitive arguments (again from left to right), and for the un-annotated parameters in the contained function expressions, we fix inferences made for type parameters referenced in the corresponding contextual type and use the inferences we have made so far to compute and assign a type. Once a type parameter is fixed in this manner, we make no further inferences for that type parameter.

This differs from the unification based type inference implemented by some functional programming languages, but it has the distinct advantage of being able to make partial inferences in incomplete code which is hugely beneficial to statement completion in IDEs. For example, see #15680 (comment) and the thread in #17237.

Now, in your example we get it right when there is an arrow function argument because we classify that as contextually sensitive and process it after first making inferences from other arguments. But we can't handle the situation where the arguments don't appear context sensitive (which the simple identifier identity doesn't) and where we actually need to process the arguments in reverse order in order to succeed.

@ahejlsberg ahejlsberg added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Jul 30, 2017
@simonbuchan
Copy link

@ahejlsberg Wouldn't lifting generic parameters in argument types work?

  • map(identity :: <T>(x: T) => T, [1, 2, 3] :: number[]) :: <T, U>(f: (x: T) => U, arr: T[]) => U[]
  • map(identity :: (x: T_from_identity) => T_from_identity, [1, 2, 3] :: number[]) :: <T_from_identity>(f: (x: T_from_identity) => T_from_identity, arr: T_from_identity) => T_from_identity[]
  • map(identity :: (x: number) => number, [1, 2, 3] :: number) :: (f: (x: number) => number, arr: number[]) => number[]

This would solve lots of other cases, like compose(), when they are given generic argument types, too. This is something that really hurts libraries like ramda and recompose.

@ahejlsberg
Copy link
Member

@simonbuchan Yes, but this type of unification gets exponentially more complicated when types aren't the simple naked type parameters in your example, but more complex constructs such as union and intersection types. For example, see #15016 (comment).

@simonbuchan
Copy link

Following up this chain, I apologise for my "smart" suggestion raising old wounds :)

Looks like #16072 gets us nearly there, thanks for that!

@mhegazy
Copy link
Contributor

mhegazy commented Aug 17, 2017

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

@mhegazy mhegazy closed this as completed Aug 17, 2017
@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.
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

4 participants