Skip to content

Generic not inferred from contextual types #25293

Closed

Description

TypeScript Version: 2.9.2

Search Terms: generic infer contextual type

Code

const baz: (n: number) => number = (
    // param `n` is correctly inferred to be `number` :-)
    n => n
);

const identity = <T>(t: T) => t;

const bar: (n: number) => number = identity(
    // param `n` is inferred as `any`, expected `number` :-(
    x => x
);

Expected behavior: TypeScript should infer the generic T in identity from the contextual type.

Actual behavior: TypeScript infers the generic T in identity as any!

Playground Link:

Related Issues:

Activity

  1. Hotell commented on Jun 28, 2018

    @Hotell

    hmm I don't think this is valid.

    What you are after is following:

    // inferred bar 👉 (x: number) => number
    const bar = identity((x: number) => x)

    by doing

    const bar: (n: number) => number = identity(
        x => x
    );

    you're telling compiler that bar should be type of (n: number) => number, but your are assigning to it identity<any>((x:any): any => x) which is inferred to type (x:any) => any and therefore you got correct error

  2. OliverJAsh commented on Jun 28, 2018

    @OliverJAsh
    ContributorAuthor

    I thought TypeScript would use bars type annotation as a "contextual type" and use it to infer the generic to identity as (n: number) => number. 🤔

  3. Hotell commented on Jun 28, 2018

    @Hotell

    I might be wrong but I don't think so, that would mean that Generics can flow deeply to function calls.

    but again explicitly annotating a variable is expressing what type should be on the right side of assignment, not to infer

  4. ahejlsberg commented on Jun 28, 2018

    @ahejlsberg
    Member

    This is working as intended. Function and arrow expression parameters without type annotations are assigned contextual types only when the contextual type is known (before instantiation) to be a function type. In your example the contextual type is simply T, so we infer from the argument (x: any) => any to T. Had the contextual type been something like (x: T) => T, we would fix the inferences we've made so far for T (which would include inferences from the return type) and then assign that type to the x parameter.

  5. Hotell commented on Jun 28, 2018

    @Hotell

    yup what Anders Hejlsberg (@ahejlsberg) and (myself partially) said :)

  6. OliverJAsh commented on Jun 28, 2018

    @OliverJAsh
    ContributorAuthor

    Thanks for the explanation Anders Hejlsberg (@ahejlsberg) and Martin Hochel (@Hotell).

    To justify why I would like this with a real world use case:

    const numberPromise = Promise.resolve(1);
    
    numberPromise.then(
      // x is inferred as number, no type annotation required
      x => x + 1
    );
    
    // Later, when I need to expand on this function, if I use `pipe` I'm
    // forced to add a type annotation to the parameter when it wasn't
    // needed before.
    
    numberPromise.then(pipe(
      // x is inferred as any, requiring me to add a type
      // annotation to the param that wasn't needed before
      x => x + 1,
      x => x * 2,
    ));
    
    numberPromise.then(pipe(
      (x: number) => x + 1,
      x => x * 2,
    ));
  7. ahejlsberg commented on Jun 28, 2018

    @ahejlsberg
    Member

    Oliver Joseph Ash (@OliverJAsh) How is pipe defined?

  8. Hotell commented on Jun 28, 2018

    @Hotell

    Hmm interesting, those definitions might be wrong ? this works

    declare const pipe: <A, R1, R2>(a: (arg: A) => R1, b: (args: R1) => R2) => (resolved: A) => R2
    
    // $Expect Promise<number>
    const numberPromise = Promise.resolve(1)
    
    numberPromise.then(
      // $Expect (x:number)=>number
      (x) => x + 1
    )
    
    // $Expect Promise<number>
    const result = numberPromise.then(
      pipe(
        // $Expect (x:number)=>number
        (x) => x + 1,
        // $Expect (x:number)=>number
        (x) => x * 2
      )
    )
  9. ahejlsberg commented on Jun 29, 2018

    @ahejlsberg
    Member

    The issue appears to be that the flow wall of overloads starts with a series of declarations that have zero parameters for the first function. In the process of matching against those, we assign type any to the x parameter of the lambda (because there is no matching contextual parameter). However, once we've made that assignment it sticks and for reasons of consistency we can't undo it. It all works if you eliminate all but the one-parameter-first-function declarations of flow. Not sure there is much we can do to change this.

  10. OliverJAsh commented on Jun 29, 2018

    @OliverJAsh
    ContributorAuthor

    Ahh, that's good to know, so we can at least workaround it by patching the flow types to remove that overload (which we don't use).

    I realised my original example was off the mark. Here is a correct example which does show the actual problem, as you described:

    // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/64104e9760065bd498a3d62d98bb05ea26500dc2/types/lodash/common/util.d.ts#L209
    declare const pipe: {
        // 0-argument first function
        <R1, R2>(f1: () => R1, f2: (a: R1) => R2): () => R2;
        // 1-argument first function
        <A1, R1, R2>(f1: (a1: A1) => R1, f2: (a: R1) => R2): (a1: A1) => R2;
    }
    
    const bar: (n: number) => number = pipe(
      // param `n` is inferred as `any`, expected `number` :-(
      x => x + 1,
      x => x * 2
    );

    If you remove the first overload, the param n is inferred as expected.

  11. OliverJAsh commented on Jul 3, 2018

    @OliverJAsh
    ContributorAuthor

    I guess this can be labelled as "won't fix"?

  12. locked as resolved and limited conversation to collaborators on Oct 21, 2025
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

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions