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

Can't correctly infer generic interface type when it's behind a function #25092

Description

@voliva

TypeScript Version: 2.9

Search Terms:
function parameter inference

Code

interface MyInterface<T> {
    retrieveGeneric: (parameter: string) => T,
    operateWithGeneric: (generic: T) => string
}

const inferTypeFn = <T>(generic: MyInterface<T>) => generic;

// inferred type for myGeneric = MyInterface<{}>, `generic.toFixed()` marked as error (as {} doesn't have .toFixed())
const myGeneric = inferTypeFn({
    retrieveGeneric: parameter => 5,
    operateWithGeneric: generic => generic.toFixed()
});

// inferred type for myGeneric = MyInterface<number>, everything OK
const myWorkingGeneric = inferTypeFn({
    retrieveGeneric: (parameter: string) => 5,
    operateWithGeneric: generic => generic.toFixed()
});

Expected behavior:
myGeneric has every type correctly inferred, parameter is a string, generic is a number.

Actual behavior:
it doesn't infer the correct type for generic parameter unless you manually specify the type of parameter (which it already had the right type)

Playground Link:
https://www.typescriptlang.org/play/#src=interface%20MyInterface%3CT%3E%20%7B%0D%0A%20%20%20%20retrieveGeneric%3A%20(parameter%3A%20string)%20%3D%3E%20T%2C%0D%0A%20%20%20%20operateWithGeneric%3A%20(generic%3A%20T)%20%3D%3E%20string%0D%0A%7D%0D%0A%0D%0Aconst%20inferTypeFn%20%3D%20%3CT%3E(generic%3A%20MyInterface%3CT%3E)%20%3D%3E%20generic%3B%0D%0A%0D%0A%2F%2F%20inferred%20type%20for%20myGeneric%20%3D%20MyInterface%3C%7B%7D%3E%2C%20%60generic.toFixed()%60%20marked%20as%20error%20(as%20%7B%7D%20doesn't%20have%20.toFixed())%0D%0Aconst%20myGeneric%20%3D%20inferTypeFn(%7B%0D%0A%20%20%20%20retrieveGeneric%3A%20parameter%20%3D%3E%205%2C%0D%0A%20%20%20%20operateWithGeneric%3A%20generic%20%3D%3E%20generic.toFixed()%0D%0A%7D)%3B%0D%0A%0D%0A%2F%2F%20inferred%20type%20for%20myGeneric%20%3D%20MyInterface%3Cnumber%3E%2C%20everything%20OK%0D%0Aconst%20myWorkingGeneric%20%3D%20inferTypeFn(%7B%0D%0A%20%20%20%20retrieveGeneric%3A%20(parameter%3A%20string)%20%3D%3E%205%2C%0D%0A%20%20%20%20operateWithGeneric%3A%20generic%20%3D%3E%20generic.toFixed()%0D%0A%7D)%3B%0D%0A%0D%0A

Activity

  1. ghost added
    BugA bug in TypeScript
    on Jun 20, 2018
  2. donaldpipowitch commented on Jul 2, 2018

    @donaldpipowitch
    Contributor

    Is this the same issue? Return type is casted to any, if generic is used:

    declare function $eval<T extends Element = HTMLElement, A = any, R = any>(
        selector: string,
        pageFunction: (element: T, ...args: A[]) => R,
        ...args: A[]
    ): Promise<ReturnType<typeof pageFunction>>;
    
    $eval<HTMLButtonElement>('.btn', (el) => el.disabled).then(value => value);
                                             // ^-- boolean             ^-- any, SHOULD be boolean
    $eval('.btn', (el) => el.spellcheck).then(value => value);
                          // ^-- boolean               ^-- boolean

    Tested on PlayGround.

  3. otbe commented on Jul 2, 2018

    @otbe

    I wonder if I got the same issue for interfaces + classes. Have a look at this playground.
    If you hover over createHandler (line 15) you will see {} as inferred type instead of string

  4. YiSiWang commented on Jul 3, 2018

    @YiSiWang

    Andy (Andrewkraft) (@Andy-MS) I got a similar issue and found a solution/workaround:

    - declare function f<T>(obj: { get: (p: number) => T, set: (v: T) => void }): T;
    + declare function f<T>(obj: { get: (p: number) => T, set: <U extends T>(v: U) => void }): T;
    declare function f<T>(obj: { get: (p: number) => T, set: <U extends T>(v: U) => void }): T;
    const res0 = f({ get: p => 0, set: v => {} });
    const res0N: number = res0; // Works!
    
    const res1 = f({ get: () => 0, set: v => {} });
    const res1N: number = res1; // Works
    
    const res2 = f({ get: p => 0, set: () => {} });
    const res2N: number = res2; // Works

    Hope it helps :) .

    Benjamin Otto (@otbe) This works.

  5. otbe commented on Jul 3, 2018

    @otbe

    14 (@YiSiWang) thanks! Never thought swapping the position would change something :)
    I wonder if this is bug or works as intended?

  6. weswigham commented on Sep 10, 2018

    @weswigham
    Member

    Inside the compiler, we have this concepts of something called a contextual type and a context-sensitive function. A contextual type is simply the type a position must have based on what it is being assigned to. A context-sensitive function is a function with untyped function parameters. When a context-sensitive function exists, its type must be derived from the contextual type at that location.

    In

    const myGeneric = inferTypeFn({
        retrieveGeneric: parameter => 5,
        operateWithGeneric: generic => generic.toFixed()
    });

    both functions in the object literal are context-sensitive. This means we try to solve their types during inference independently. We see parameter unannotated first, lock in {} as the inference (since we decide that parameter must have a type before we can check the body), and then that's what it's stuck with.

    whereas

    const myWorkingGeneric = inferTypeFn({
        retrieveGeneric: (parameter: string) => 5,
        operateWithGeneric: generic => generic.toFixed()
    });

    only operateWithGeneric is context-sensitive. This means we create an inference of T -> string before we even look at the context sensitive function (since we do context-free inferences and then context-sensitive ones) and everything's good.

    We're conservative here because we assume that a parameter, if present, may affect the return type of a function. Based on what we've talked about in related issues before, it's unlikely we'll specifically improve this without moving to a full unification-based solve for the arguments. the recommendation right now is to not list parameters you don't use (or if you must list them, manually assign them types). ❤️

  7. added
    Design LimitationConstraints of the existing architecture prevent this from being fixed
    and removed
    BugA bug in TypeScript
    Needs InvestigationThis issue needs a team member to investigate its status.
    on Sep 13, 2018
  8. sutarmin commented on Oct 17, 2018

    @sutarmin

    Wesley Wigham (@weswigham) I wonder if following piece of code is related to this issue:

    declare function test<T>(a: T, b: T): void;
    declare const a: { cb: (arg: number) => number }
    test(a, { cb: arg => arg }) // [ts] Parameter 'arg' implicitly has an 'any' type.

    We have type checking here ((arg: string) => arg won't fit), but don't have type inference.
    Is it the same problem or something different?

  9. raveclassic commented on Oct 17, 2018

    @raveclassic

    Might be related #23429, #22715 #15005

    I've added a compete example of Anton Sutarmin (@sutarmin) 's problem here

  10. ferdaber commented on Mar 7, 2019

    @ferdaber

    Wesley Wigham (@weswigham) I should add that this affects JSX as well, with generic components:

    interface MyInterface<T> {
      retrieveGeneric: (parameter: string) => T,
      operateWithGeneric: (generic: T) => string
    }
    
    export declare function Component<T>(props: MyInterface<T>): JSX.Element
    
    const element = (
      <Component
        operateWithGeneric={ generic => generic.toFixed() } // <- error here with generic being `{}`
        retrieveGeneric={ parameter => 5 }
      />
    )
  11. RyanCavanaugh commented on Mar 13, 2019

    @RyanCavanaugh
    Member

    Tracking at #30134

  12. csr632 commented on Nov 30, 2019

    @csr632
    declare function f<T>(obj: { get: (p: number) => T, set: <U extends T>(v: U) => void }): T;
    const res0 = f({ get: p => 0, set: v => {} });
    const res0N: number = res0; // Works!
    
    const res1 = f({ get: () => 0, set: v => {} });
    const res1N: number = res1; // Works
    
    const res2 = f({ get: p => 0, set: () => {} });
    const res2N: number = res2; // Works

    It will report error when you do this:

    declare function f<T>(obj: { get: (p: number) => T, set: <U extends T>(v: U) => void }): T;
    
    const res0 = f({
        get: p => 0, set: v => {
            // error!
            return v.toFixed()
        }
    });

    This is so wierd.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Design LimitationConstraints of the existing architecture prevent this from being fixedFix AvailableA PR has been opened for this issue

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions