Skip to content

Type inference of generics is order-dependent #6627

Closed

Description

declare var f: { 
    <U>(x: U): U,
};
declare function pipe<T>(arg: T, dit: (arg: T) => T): T; // welkom in F#!
declare function apply<V>(dit: (arg: V) => V, arg: V): V;
let n = pipe(12, f);
let m = apply(f, 12);

n: number as desired but m: {}. It looks like, during the second pass of type parameter inference for apply, the call signature is inferred from dit's argument before it is inferred from arg. Since f is itself generic, the default is {}. Anders Hejlsberg (@ahejlsberg), is this intended behaviour? It seems like a bug to me.

Also I am not sure why getSingleCallSignature special-cases single-call-signature-only objects. You get yet a different result if you have an extra property on f:

declare var f: { 
    <U>(x: U): U,
    hunch(x: number): number
};
...
let n = pipe(12, f)
let m = apply(f, 12)

Now both n: any and m: any because getSingleCallSignature doesn't return a signature, so instantiateTypeWithSingleGenericCallSignature doesn't do its instantiation at all.

Activity

  1. sandersn commented on Jan 26, 2016

    @sandersn
    MemberAuthor

    A normally specified function produces the same result:

    function g<U>(u: U) { return u }
    let m = apply(g, 12) // m: {}
  2. DanielRosenwasser commented on Jan 26, 2016

    @DanielRosenwasser
    Member

    What is u in the above example?

  3. sandersn commented on Jan 26, 2016

    @sandersn
    MemberAuthor

    A typo, should be apply(g, 12)

  4. ahejlsberg commented on Jan 27, 2016

    @ahejlsberg
    Member

    When the type of an argument has a generic call signature, there are two ways we could infer from the argument:

    A. Instantiate it in the context of the corresponding parameter type and infer from the resulting type, or
    B. Ignore the generic call signature and infer from the other properties of the object.

    If the object has a single generic call signature and no other members there's no doubt we're dealing with a generic function and we use method A. Otherwise, it's not clear what we're dealing with, so we use method B (i.e. normal inference).

    When we're using method A we may be forced to fix inferences we've already made for some type parameters as described in section 4.15.2. That's what you're seeing here.

    Specifically, in the call pipe(12, f) we first infer number for T from the first argument and then the second argument causes that inference to be fixed an applied using method A. But in the call to apply(f, 12) we haven't made any inferences for T by the time we fix it, so we get {}. Ideally we want to back off in the second example and handle the function argument in a second pass, but we can't tell from the shape of the argument that this is the case. We only postpone type inference for arguments that syntactically indicate they need it (this is the purpose of the isContextSensitive function in the checker).

    So, long story short, this is the expected behavior given our current design.

  5. sandersn commented on Jan 27, 2016

    @sandersn
    MemberAuthor

    Ah, so if we wrap f in a lambda, we get the right answer:

    let m = apply(x => f(x), 12)
  6. locked and limited conversation to collaborators on Jun 19, 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

    By DesignDeprecated - use "Working as Intended" or "Design Limitation" instead

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions