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

Wrong overload resolution with generics #11343

Closed
aikoven opened this issue Oct 4, 2016 · 7 comments
Closed

Wrong overload resolution with generics #11343

aikoven opened this issue Oct 4, 2016 · 7 comments
Labels
Duplicate An existing issue was already created

Comments

@aikoven
Copy link

aikoven commented Oct 4, 2016

TypeScript Version: 1.8.10

Code

declare function compose<A, B, R>(f1: (b: B) => R, f2: (a: A) => B): (a: A) => R;

declare function compose<R>(...funcs: Function[]): (arg: any) => R;


type Creator<S> = () => S;
type Enhancer = <S>(creator: Creator<S>) => Creator<S>;

declare const enhancer1: Enhancer;
declare const enhancer2: Enhancer;

const enhancer = compose(enhancer1, enhancer2);

const creator = enhancer(() => '');
creator();

Expected behavior:
Should compile with no error.

Actual behavior:
Compiler gives error on the last line:

Uncaught Semantic: Cannot invoke an expression whose type lacks a call signature.

However if we remove the second oveload of compose, everything works fine, which means that the first overload actually matches but for some reason the second one is selected.

Also, if we remove type variable from Enhancer definition and replace it with string, it compiles again.

@gcnew
Copy link
Contributor

gcnew commented Oct 4, 2016

TypeScript is not good at preserving templated types (i.e. inferring most general type of polymorphic functions). This is generally a duplicate of #9949. Note that even if you comment out the second overload, the types are still not correct, as the template param S of Enhancer is substituted with {}. I.e. the inferred signature for enhancer is:

creator: (a: {}) => () => {}

While the correct one would be:

creator: <T>(a: T) => () => T

PS: It is somewhat related to FAQ - Generics. Unreferenced generic parameters are a no-no as of now :(.

@aikoven
Copy link
Author

aikoven commented Oct 4, 2016

I'm not sure I understand. I acknowledge that TypeScript is unable to preserve type argument and the best we can get from compose is (creator: Creator<{}>) => Creator<{}>

The main question is: if TypeScript selects the first matching overload, and the first one actually matches, then why does it selects the second one?

@gcnew
Copy link
Contributor

gcnew commented Oct 4, 2016

I see now, yes it does seem odd.

@gcnew
Copy link
Contributor

gcnew commented Oct 4, 2016

This behaviour is triggered whenever the return type must be inferred.

The following works as expected:

declare const f: (x:number) => string;
declare const g: (x:boolean) => number;
const c = compose(f, g) // (a: boolean) => string

However if we change f to return a generic, it is the second overload that gets selected:

declare const f: <T>(x:number) => T;
declare const g: (x:boolean) => number;
const c = compose(f, g) // (arg: any) => {}

The above example is logically incorrect, because we can't produce a value of type T for any arbitrary type T just from the input argument x: number. In this case TS is being lax and chooses the signature that has no restrictions.

Your example, however, is a special case, because the given signature of Enhancer the argument and the return have the same type.

type Enhancer = <S>(creator: Creator<S>) => Creator<S>;

However, TypeScript doesn't seem to be able to notice that and doesn't use the argument creator as an inference point, thus it falls back again to the more lax signature.

Onto the question, why does the inferencer try harder when there is no second signature - I don't know, but the inferred type is not more useful. If template types were preserved, then from the inferencer's point of view Creator<S> would be just a rigid type and the signature would just have to be checked. Now they have to be inferred as well.

Edit: removed identity observations, because they are not pertinent to the real type of Store described below

@aikoven
Copy link
Author

aikoven commented Oct 4, 2016

Our use case is creating Redux Store:

interface Store<S> {
  getState(): S;
  // bunch of other methods
}

And actually the signature of Creator is more like:

type Creator<S> = (...params) => Store<S>;

I just tried to get the minimal failing example.

Enhancer doesn't have to be identity, it may take one creator and produce another that e.g. creates the store, does some behaviour enhancements on it and returns the new one without altering its state type S.

@mhegazy mhegazy added the Duplicate An existing issue was already created label May 17, 2017
@mhegazy
Copy link
Contributor

mhegazy commented May 17, 2017

looks like a duplicate of #9949

@mhegazy
Copy link
Contributor

mhegazy commented May 18, 2017

Actually a better one to dupe against is #9366

@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

3 participants