镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

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

Description

@1ven

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

Activity

  1. ahejlsberg commented on Jul 30, 2017

    @ahejlsberg
    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.

  2. simonbuchan commented on Jul 31, 2017

    @simonbuchan

    Anders Hejlsberg (@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.

  3. ahejlsberg commented on Jul 31, 2017

    @ahejlsberg
    Member

    Simon Buchan (@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).

  4. simonbuchan commented on Jul 31, 2017

    @simonbuchan

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

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

  5. mhegazy commented on Aug 17, 2017

    @mhegazy
    Contributor

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

  6. locked and limited conversation to collaborators on Jun 14, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Design LimitationConstraints of the existing architecture prevent this from being fixed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions