Skip to content

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

Closed

Description

TypeScript Version: [email protected]

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

Activity

  1. 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>[/+] on Jun 7, 2019
  2. fatcerberus commented on Jun 7, 2019

    @fatcerberus

    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.

  3. fatcerberus commented on Jun 7, 2019

    @fatcerberus

    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.

  4. Jamesernator commented on Jun 7, 2019

    @Jamesernator
    Author

    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

  5. fatcerberus commented on Jun 7, 2019

    @fatcerberus

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

  6. jack-williams commented on Jun 7, 2019

    @jack-williams
    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);
  7. fatcerberus commented on Jun 7, 2019

    @fatcerberus

    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 (@jack-williams)!

  8. ahejlsberg commented on Jun 7, 2019

    @ahejlsberg
    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 (@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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Design LimitationConstraints of the existing architecture prevent this from being fixed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions