Skip to content

ReturnType returns unknown if function's return type is generic argument of the function (with default value) that has not been set #57463

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

Closed
cts-tradeit opened this issue Feb 21, 2024 · 15 comments

Comments

@cts-tradeit
Copy link

🔎 Search Terms

ReturnType, generic, default

🕗 Version & Regression Information

  • This is a crash
  • This changed between versions ______ and _______
  • This changed in commit or PR _______
  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about _________
  • I was unable to test this on prior versions because _______

⏯ Playground Link

No response

💻 Code

const func = <Return = string>(): Return {
   //
}

type ShouldBeNumber = ReturnType<typeof func<number>>; // number
type ShouldBeString = ReturnType<typeof func>; // unknown

🙁 Actual behavior

ReturnType returns unknown.

🙂 Expected behavior

ReturnType returns default type.

Additional information about the issue

This was encountered when trying to write this kind of function:

const invoker  = <Endpoint extends (...args: any) => any>(endpoint: Endpoint, ...parameters: Parameters<Endpoint>): ReturnType<Endpoint> {
    // Perform some logic before endpoint function is called

    return endpoint(...parameters);
}
@MartinJohns
Copy link
Contributor

Duplicate of #48870.

@cts-tradeit
Copy link
Author

cts-tradeit commented Feb 21, 2024

I do not think this is working as indented in sense that it might be an intentional side effect but it is frankly stupid to claim that this is "proper" behaviour that should be upheld.

If this is working

type ExtractDefault<T> = T extends infer U ? U : never;
type MyType<T = number> = T;
type DefaultType = ExtractDefault<MyType>; // number, NOT unknown

I see no reason why it should not work with functions.

@fatcerberus
Copy link

fatcerberus commented Feb 21, 2024

That exhibits the same distinction as

type Fun1<T = string> = () => T;
type Fun2 = <T = string>() => T;

One is a generic type (i.e. a type-function) that resolves to a concrete, non-generic function type. The other is the type of a generic function, whose return type is not known until it’s called.

Note that this is the same reason you can’t write Fun2<number>.

@rotu
Copy link

rotu commented Feb 21, 2024

One is a generic type (i.e. a type-function) that resolves to a concrete function type. The other is the type of a generic function, whose return type is not known until it’s called.

I agree on the reasoning, but also that this is a silly way for the program to behave.

What's really intended is an instantiation expression with an empty type arguments list, e.g.:

// note: this gives an error:
// Type argument list cannot be empty.
type ShouldBeString = ReturnType< typeof func<> >;

A workaround (that I hate) is to add a dummy variable, so you can create an instantiation expression:

const func = <IGNORED, Return = string>(x:T): T =>{
  throw new Error("implement me")
}
type F1 = typeof func<null>
type F2 = typeof func<null,string>

@cts-tradeit
Copy link
Author

cts-tradeit commented Feb 21, 2024

@fatcerberus and what if I do this?

type Fun2<T = string> = () => T;

type L = Fun2<number>;

Do not get me wrong I understand that this is "how typescript was designed to work". But I do think that this distinguishment is frankly odd.

If not removing this split competely, some expression that can be used to convert one to another might fix this issue?

@rotu
Copy link

rotu commented Feb 21, 2024

If not removing this split competely, some expression that can be used to convert one to another might fix this issue?

Is this what you're looking for?

const func = <Return,>(): Return => {
  throw new Error("implement me")
}
type Func<T=string,> = typeof func<T>
type ShouldBeNumber = ReturnType<Func<number>>; // number
//   ^?
type ShouldBeString = ReturnType<Func>; // string
//   ^?

https://www.typescriptlang.org/play?#code/MYewdgzgLgBAZgVzMGBeGAeASgUyggJzABoA+ACgEoAuGXfItUmAbwCgYYoALAkAdxhgcggKIE+BcgCIAlgFsADgBsc8nGFjrplNgF82UAJ6KcMAGJJgGACqpoBWWADmZNFxM4QceFdulDTxgAZW4QBGUAEwAhHAA5BHkAIxwCd3pCMBtPDEtkDDBElIJSUgBuGAB6SqEi1LZqzhgAPQB+QNMQsIiYnGCoRxd0vEzs01yrcqqahydnBprONqA

@fatcerberus
Copy link

fatcerberus commented Feb 21, 2024

@cts-tradeit In case it's not clear, the reason for the distinction is:

type Fun1<T = string> = () => T;
type Fun2 = <T = string>() => T;

let x: Fun1  = () => "foo";  // ok
let y: Fun1 = () => 42;  // not ok, number is not assignable to string
let z: Fun2 = () => "foo";  // not ok, () => string can't function as a generic
let w: Fun2 = <T,>() => ({}) as T;  // ok

Without this distinction, you'd have no type you could use for w, which is important when you want to use generic functions as HOFs.

@cts-tradeit
Copy link
Author

cts-tradeit commented Feb 21, 2024

@rotu No not exactly. Imagine using a package (which i cannot alter) that exports the function below. Why the singature looks like this? Who knows. The author chose it for some reason. Beyond this function there are 50 more that have similar generic gimmick.

export const loadData = <Data = BasicData>(): Promise<Data> => {
   // omitted
}

Now, I need to know what type the function loadData defaults to when generic is not explicitly defined. See the function invoker in the first post. Your proposed solution would require me to explicitly define type for every single one of the 50 functions.

@fatcerberus
Copy link

I think this might be close to what you need: #40179

@rotu
Copy link

rotu commented Feb 21, 2024

export const loadData = <Data = BasicData>(): Promise<Data> => {
   // omitted
}

Now, I need to know what type the function loadData defaults to when generic is not explicitly defined.

From that signature, you can't know what loadData will return.

If the author wants to communicate an actual type bound, it's with an extends clause:

export const loadData = <Data extends BasicData,>(): Promise<Data> => {
   // omitted
}

In this case, ReturnType<typeof loadData> will be Promise<BasicData>

@cts-tradeit
Copy link
Author

cts-tradeit commented Feb 21, 2024

@fatcerberus If i understood the proposal correctly, it would allow me to do something like this:

type Func = <T = string>(param: number) => T;

type FuncParams = Parameters<Func>;

type FuncReturn = ValueForArguments<Func, FuncParams>;  // string

@cts-tradeit
Copy link
Author

cts-tradeit commented Feb 21, 2024

@rotu what about adding something like this to the language?

export const loadData = <Data = BasicData>(): Promise<Data> => {
   // omitted
}

type R = ReturnType<typeof loadData<default>>; // BasicData

I as the caller of the function know that I do not plan on specifying the generic argument any other than the default. It would be nice to be able to hint this to the language. U might argue that I may write the default type instead of default but this would require me to do it 50 times (a repeat this every time the author changes the type). The example above is not neceserily what I aim for, but I demonstrates the idea of hinting that I want to use the default value.

@rotu
Copy link

rotu commented Feb 21, 2024

@rotu what about adding something like this to the language?

type R = ReturnType<typeof loadData<default>>; // BasicData

I think I'd prefer if ReturnType<typeof loadData<>> were legal, since it's a straightforward extrapolation of the existing syntax and fits with existing convention (e.g. you don't write [default] for an empty list). I'm kind of surprised that the empty type parameter list doesn't work, given that you can declare a type with an empty list of type variables: type Foo<> = {}.

I do think it's ugly that type variables in generic functions can have defaults. I don't see a use for them versus extends constraints.

@RyanCavanaugh
Copy link
Member

I do not think this is working as indented in sense that it might be an intentional side effect but it is frankly stupid to claim that this is "proper" behaviour that should be upheld.

If decisions we make either agree with you or are stupid, then I don't see much point in trying to engage with this issue. Seems like other folks have covered all the relevant points here - there are open issues on multiple possible avenues of solution for the particular problem.

@RyanCavanaugh RyanCavanaugh closed this as not planned Won't fix, can't repro, duplicate, stale Feb 21, 2024
@cts-tradeit
Copy link
Author

cts-tradeit commented Feb 21, 2024

@RyanCavanaugh That is called false dichotomy. There are certainly more possibilities than these two. But honestly my hopes for this to be fixed were zero to resolved. However, it is good to raise awarness about shorcomings of the language. In few month, someone else will report this once again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants