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

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

Closed
voliva opened this issue Jun 20, 2018 · 11 comments · Fixed by #48538
Closed

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

voliva opened this issue Jun 20, 2018 · 11 comments · Fixed by #48538
Assignees
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed Fix Available A PR has been opened for this issue

Comments

@voliva
Copy link

voliva commented Jun 20, 2018

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

@ghost ghost added the Bug A bug in TypeScript label Jun 20, 2018
@ghost
Copy link

ghost commented Jun 20, 2018

The error doesn't happen if at least one of the parameters is not declared in the callback:

declare function f<T>(obj: { get: (p: number) => T, set: (v: T) => void }): T;
const res0 = f({ get: p => 0, set: v => {} });
const res0N: number = res0; // Error

const res1 = f({ get: () => 0, set: v => {} });
const res1N: number = res1; // Works

const res2 = f({ get: p => 0, set: () => {} });
const res2N: number = res2; // Works

@donaldpipowitch
Copy link
Contributor

donaldpipowitch commented Jul 2, 2018

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.

@otbe
Copy link

otbe commented Jul 2, 2018

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

@YiSiWang
Copy link

YiSiWang commented Jul 3, 2018

@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 :) .

@otbe This works.

@otbe
Copy link

otbe commented Jul 3, 2018

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

@mhegazy mhegazy added this to the TypeScript 3.1 milestone Jul 23, 2018
@mhegazy mhegazy added the Needs Investigation This issue needs a team member to investigate its status. label Jul 23, 2018
@weswigham
Copy link
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). ❤️

@weswigham weswigham added Design Limitation Constraints of the existing architecture prevent this from being fixed and removed Bug A bug in TypeScript Needs Investigation This issue needs a team member to investigate its status. labels Sep 13, 2018
@sutarmin
Copy link

@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?

@raveclassic
Copy link

Might be related #23429, #22715 #15005

I've added a compete example of @sutarmin 's problem here

@ferdaber
Copy link

ferdaber commented Mar 7, 2019

@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 }
  />
)

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Mar 13, 2019

Tracking at #30134

@csr632
Copy link

csr632 commented Nov 30, 2019

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
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed Fix Available A PR has been opened for this issue
Projects
None yet