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
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.
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
both functions in the object literal are context-sensitive. This means we try to solve their types during inference independently. We see
parameterunannotated first, lock in{}as the inference (since we decide thatparametermust have a type before we can check the body), and then that's what it's stuck with.whereas
only
operateWithGenericis context-sensitive. This means we create an inference ofT -> stringbefore 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). 鉂わ笍