Typescript: Can't correctly infer generic interface type when it's behind a function

Created on 20 Jun 2018  路  11Comments  路  Source: microsoft/TypeScript


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

Design Limitation

Most helpful comment

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). 鉂わ笍

All 11 comments

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

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.

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

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

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

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

Might be related #23429, #22715 #15005

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

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

Tracking at #30134

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Antony-Jones picture Antony-Jones  路  3Comments

siddjain picture siddjain  路  3Comments

zhuravlikjb picture zhuravlikjb  路  3Comments

blendsdk picture blendsdk  路  3Comments

manekinekko picture manekinekko  路  3Comments