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 assign result of function of type T to ReturnType<T> #31811

Closed
Jamesernator opened this issue Jun 7, 2019 · 7 comments
Closed

Can't assign result of function of type T to ReturnType<T> #31811

Jamesernator opened this issue Jun 7, 2019 · 7 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@Jamesernator
Copy link

Jamesernator commented Jun 7, 2019

TypeScript Version: typescript@3.6.0-dev.20190606

Search Terms:
generic ReturnType, ReturnType assign

Explanation

TypeScript seems to treat the return type of a generic not as the ReturnType<T> but the type it extends from. Basically in this example we get a type error (even though instance is correctly inferred as Foo) because IFoo is not assignable to ReturnType<T>:

interface IFoo {
    someMethod(): string;
}

class Foo implements IFoo {
    static create(p: number) {
        return new Foo(p)
    }

    constructor(readonly p: number) { }
    someMethod = () => 'a';
}

function createSomeInterfaceInstance<T extends (p: number) => IFoo>(factoryFunction: T): ReturnType<T> {
    // This is a TypeError that IFoo is not assignable to ReturnType<T>
    return factoryFunction(12)
}

const instance = createSomeInterfaceInstance(Foo.create);

Now this is hackable-around in two different ways, one by explicitly adding a generic for the return type of T e.g.:

function createSomeInterfaceInstance<K extends IFoo, T extends (p: number) => K>(factoryFunction: T): K

Or just by using a type intersection on the return type e.g.:

function createSomeInterfaceInstance<T extends (p: number) => IFoo & ReturnType<T>>(factoryFunction: T): ReturnType<T>

But both of these solutions feel pretty hacky, it would instead be better if TypeScript inferred the type of factoryFunction(10) to be IFoo & ReturnType<T> instead of just IFoo.

Playground Link: playground-link

Related Issues:

Probably related: #29133

@Jamesernator Jamesernator changed the title Can't assign result of function of T to ReturnType<T> Can't assign result of function of type T to ReturnType<T> Jun 7, 2019
@fatcerberus
Copy link

fatcerberus commented Jun 7, 2019

The error actually kind of makes sense to me, at least.

T extends (p: number) => IFoo

factoryFunction (as far as TypeScript knows) returns an IFoo. Return types are covariant, so T could in fact end up to be (p: number) => ISpecialFoo and then assigning IFoo to that wouldn't be sound.

I guess the compiler isn't smart enough to realize that ReturnType<T> is itself generic.

@fatcerberus
Copy link

Hmm, the plot thickens. The IntelliSense hover text for factoryFunction says:

(p: number) => IFoo

Normally for variables of generic types it will say the type is T extends ..., here it seems to have just decided the function is the concrete type of its constraint. So it's nothing to do with ReturnType but rather how TS has inferred the type of the function call itself.

@Jamesernator
Copy link
Author

Jamesernator commented Jun 7, 2019

but rather how TS has inferred the type of the function call itself.

It seems to infer the type of instance just fine and T is (p: number) => Foo rather than (p: number) => IFoo so the fact it can't do it within the function seems surprising:

Screen Shot 2019-06-07 at 1 51 02 pm

@fatcerberus
Copy link

Well, within the body of the generic function it doesn't actually know what T is (hence why it's generic 😉), so that much makes sense at least. What doesn't make sense to me is why it thinks factoryFunction is concretely (p: number) => IFoo instead of T extends (p: number) => IFoo, like it would usually be.

Let's see if someone else smarter comes along to solve this mystery because I'm out of ideas. 😛

@jack-williams
Copy link
Collaborator

The type-checking looks reasonable - it knows that factory function is generic, it's just the quick info that is slightly off.

function createSomeInterfaceInstance<T extends (p: number) => IFoo>(factoryFunction: T): ReturnType<T> {
    const fun: T = factoryFunction; // hover factoryFunction: (parameter) factoryFunction: T extends (p: number) => IFoo
    return fun(12); // hover fun: const fun: T (p: number) => IFoo
    //                                       ^ the generic is here, but in a weird place.
}

TypeScript doesn't know how to apply a generic parameter, so it attempts to find a function-like constraint and apply that - this is why the return type is IFoo. I guess you want the application of a parameter T to return ReturnType<T>? Not sure how tractable this is once things get more complicated; it is hard to do anything with a type ReturnType<T>.

The solution:

function createSomeInterfaceInstance<K extends IFoo, T extends (p: number) => K>(factoryFunction: T): K

Doesn't feel hacky to me and is inline with TypeScript guidelines that suggests pushing type-parameters down as far as possible. Though I would write it as follows, unless you really need T:

function createSomeInterfaceInstance<R>(factoryFunction: (p: number) => R): R {
    return factoryFunction(12); 
}

const instance: Foo = createSomeInterfaceInstance(Foo.create);

@fatcerberus
Copy link

fatcerberus commented Jun 7, 2019

it is hard to do anything with a type ReturnType<T>.

You're right about that - I overlooked that ReturnType is conditional; you can't actually assign stuff to a conditional type (without a cast) until it's been resolved to a real type, but since there's a generic type variable T involved, that hasn't happened yet.

ReturnType<T> in this context is essentially an existential type (also this) and TypeScript doesn't actually support those. 😅

Thanks @jack-williams!

@ahejlsberg
Copy link
Member

In order for the original example to work the type checker would have to consider the type of the call expression factoryFunction(12) to be a higher order ReturnType<T> instead of falling back to the return type of the constraint of T (i.e. IFoo). This is not something that is currently on our radar.

I'll echo what @jack-williams said above. The best way to write it is:

function createSomeInterfaceInstance<R>(factoryFunction: (p: number) => R): R {
    return factoryFunction(12); 
}

const instance: Foo = createSomeInterfaceInstance(Foo.create);

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
Projects
None yet
Development

No branches or pull requests

5 participants