Skip to content

Generics; ReturnType<Foo> != ReturnType<typeof foo> #24277

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

Open
AnyhowStep opened this issue May 20, 2018 · 7 comments
Open

Generics; ReturnType<Foo> != ReturnType<typeof foo> #24277

AnyhowStep opened this issue May 20, 2018 · 7 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@AnyhowStep
Copy link
Contributor

TypeScript Version: Version 2.9.0-dev.20180516

Search Terms: generic function returntype

I couldn't think of a good title for this,

Code

type Delegate<T> = () => T;

function executeGeneric<
    X,
    DelegateT extends Delegate<X> 
>(delegate: DelegateT): ReturnType<DelegateT> {
    //Type 'X' is not assignable to type 'ReturnType<DelegateT>'.
    const x: ReturnType<typeof delegate> = delegate();
    //Type 'X' is not assignable to type 'ReturnType<DelegateT>'.
    const y: ReturnType<DelegateT> = delegate();
    //Type 'X' is not assignable to type 'ReturnType<DelegateT>'.
    return delegate();
}

Expected behavior:

Return successfully.

Intuitively, to me,

  1. delegate is of typeDelegateT.
  2. ReturnType<DelegateT> and ReturnType<typeof delegate> should be the same

However,

Actual behavior:

Type 'X' is not assignable to type 'ReturnType<DelegateT>'.

Playground Link: Here

Related Issues:

@mhegazy
Copy link
Contributor

mhegazy commented May 21, 2018

ReturnType<DelegateT> and ReturnType<typeof delegate> are the same, and both are not assignable from DelegateT, you can not assign a function that returns a string to a string.

@mhegazy mhegazy added the Question An issue which isn't directly actionable in code label May 21, 2018
@AnyhowStep
Copy link
Contributor Author

Isn't delegate of type DelegateT and delegate() of type ReturnType<DelegateT>?

delegate() is not supposed to be X, it could be X but it could also be something that extends X

But it's saying that delegate() is of type X when it shouldn't be (intuitively, to me, anyway)

@mhegazy
Copy link
Contributor

mhegazy commented May 21, 2018

ah. sorry about that, did not see the call there.. yes. you are right about that.

@mhegazy mhegazy added Bug A bug in TypeScript and removed Question An issue which isn't directly actionable in code labels May 21, 2018
@mhegazy mhegazy added this to the TypeScript 3.1 milestone Jul 16, 2018
@weswigham weswigham added the Domain: Conditional Types The issue relates to conditional types label Nov 13, 2018
@AnyhowStep
Copy link
Contributor Author

I don't know why I didn't think of this shorter example.

function executeGeneric<
    DelegateT extends () => number
>(delegate: DelegateT): ReturnType<DelegateT> {
    //Type 'number' is not assignable to type 'ReturnType<DelegateT>'.
    const x: ReturnType<typeof delegate> = delegate();
    //Type 'number' is not assignable to type 'ReturnType<DelegateT>'.
    const y: ReturnType<DelegateT> = delegate();
    //Type 'number' is not assignable to type 'ReturnType<DelegateT>'.
    return delegate();
}

Playground

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript In Discussion Not yet reached consensus and removed Bug A bug in TypeScript Domain: Conditional Types The issue relates to conditional types labels Mar 13, 2019
@RyanCavanaugh RyanCavanaugh removed this from the TypeScript 3.4.0 milestone Mar 13, 2019
@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature and removed In Discussion Not yet reached consensus labels Mar 13, 2019
@RyanCavanaugh
Copy link
Member

This is a suggestion, not a bug.

Direct function calls are evaluated according to their constraints. This is generally desirable:

function invokeAndMultiply<T extends () => number>(a: T, b: T) {
    // OK or no?
    return a() * b();
}

In general you cannot multiply arbitrary types, so this example would be an error if we forced deferral of a()'s return type to ReturnType<T>.

It'd be a pretty substantial complexity trade-off to start deferring the return type evaluation, since anything you do with a generic type constrained to a function type would then need follow-on special handling.

@AnyhowStep
Copy link
Contributor Author

AnyhowStep commented May 24, 2019

Let's say we defer the evaluation of a()'s return type.

In this example, even if TS hasn't evaluated what ReturnType<T> is,
it should know that ReturnType<T> extends number.

And because of that, it should allow their multiplication, because you can multiply two number types and ReturnType<T> is a subtype of number; the result being just number.


My example is simple. So, deferring the evaluation of the type is also not an issue.

In general, if given this,

function foo<F extends () => T> (f : F) : ReturnType<F> {
    /*
        Current behaviour:
        `value` is of type `T`.

        Proposed behaviour:
        `value` is of type `(T & ReturnType<F>)`,
        where evaluation of `ReturnType<F>` is deferred.
    */
    const value = f();
    //Right now, we can do all things `T` can do because `value` is subtype of `T`.

    /*
        This should now work because `(T & ReturnType<F>)` extends `ReturnType<F>`.
    */
    return value;
}

Actually, I don't know anything about the compiler internals but I'm quite pleased with the above suggestion =x
Is there anything about it that stands out as potentially dangerous?

@jtbandes
Copy link
Contributor

What additional feedback would be useful on this request? I filed a duplicate in #47900.

My real-world use case boils down to this:

interface IReadable {
  read(): number | string;
}
class Reader<R extends IReadable> {
  constructor(private _reader: R) {}

  read(): ReturnType<R["read"]> {
    return this._reader.read();  // error :(
  }
}

let num = new Reader({ read: () => 1 }).read();  // inferred as number
let str = new Reader({ read: () => "1" }).read();  // inferred as string

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

6 participants