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

Simplify return type of async function #43303

Closed
5 tasks done
jakubnavratil opened this issue Mar 18, 2021 · 14 comments
Closed
5 tasks done

Simplify return type of async function #43303

jakubnavratil opened this issue Mar 18, 2021 · 14 comments
Labels
Duplicate An existing issue was already created

Comments

@jakubnavratil
Copy link

jakubnavratil commented Mar 18, 2021

Suggestion

Allow typing async function without generic Promise type.

πŸ” Search Terms

async, promise, return type

βœ… Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

Async functions always return Promise, so there is no need to specificly write return type of that functions as Promise<type>.
Combination of async function and simple return type should be enough for typesystem to handle that.

Instead:

async fn(): Promise<number[]>

This:

async fn(): number[];  // type system infers return type as Promise<number[]> automaticaly.

Currently results in TS error:
The return type of an async function or method must be the global Promise<T> type. Did you mean to write 'Promise<any>'?ts(1064)

So TS clearly knows it MUST be Promise, but requires redundant annotation to prove it.

πŸ’» Use Cases

This is particulary useful for projects, which enforces writing down function return types (Angular, ...)
Writing Promise<X> is already redundant as just second before you wrote async while defining function.
Also converting function to async function requires changing return type.
Should be backwards compatible.

Overall my view of typesystem is to add benefit to developer with minimal action. Because of that, we have very good type infering.
This seems to me like handy addition.
And I'm not alone #40425 (comment)

In my head, this doesn't sound too complicated to implement πŸ˜„

@MartinJohns
Copy link
Contributor

MartinJohns commented Mar 18, 2021

This is basically a worse version of #40425.

And an exact duplicate of #32551 / #7284.

@jakubnavratil
Copy link
Author

jakubnavratil commented Mar 19, 2021

Well I don't see how this is worse than #40425 . This does not suggest new syntax to language for no reason.
I see the duplication with #7284 but the main comment againts this was flawed in my opinion (#7284 (comment))

From what I can tell, functions with async keyword returns Promise and nothing else. So declaration:

async getBar(): Thenable<Bar>

is not possible as TS require:
The return type of an async function or method must be the global Promise<T> type. Did you mean to write 'Promise<any>'?ts(1064)

You CAN always return Promise object from any function, but async functions always returns Promise and nothing else (#6686 (comment))

Suggestion was closed here #7284 (comment)
But its 5 years old and async/await is widely used everywhere.

Main issue is that this requires redundancy on function declaration. Similiar as writing const value: number = 1; which is usually bad practice same as writing any redundant code is usually bad practice.

I get argument that async keyword is visually disconnected from type annotation, but it is also highlighted keyword usually first on the line.

This behavior could be optional, maybe this could be discussed again?

async fn(): number; // infers Promise<number>
async fn(): boolean | number; // infers Promise<boolean | number>
async fn(): Promise<boolean | number>;
fn(): Promise<number>

Some more info: #33595 (comment)
This seems to me more consistent:

async fn(): number { // <-----|
  return 3.14; // number  <---|
}

than this:

async fn(): Promise<number> {
  return 3.14; // number
}

@MartinJohns
Copy link
Contributor

MartinJohns commented Mar 19, 2021

Note that you're wrong assuming that every async function will return a Promise<T> type. Async generator functions return AsyncGenerator<...>.

And I personally would find it really confusing if the following would compile:

interface E { x(): Promise<number>; }
class A implements E {
    async x(): number { return 5; }
}

@jakubnavratil
Copy link
Author

Note that you're wrong assuming that every async function will return a Promise<T> type. Async generator functions return AsyncGenerator<...>.

Well technicaly you are right. But this is about async functions that return Promise not about async generators. Still valid point tho.

And I personally would find it really confusing if the following would compile:

interface E { x(): Promise<number>; }
class A implements E {
    async x(): number { return 5; }
}

I agree this is subjective. Looking from inside the functions, types match. Looking from outside, they dont.
Still in your example you have async keyword and you don't need to write redundant Promise<number> annotation.

Nonetheless, this is suggestion as this bugs me in code on deaily basis and I wouldn't be cofused by this.

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Mar 19, 2021
@RyanCavanaugh
Copy link
Member

This just creates new levels of confusion that are very difficult to reason about, because when you write async foo(): Promise<T>, that's clearly not intended to mean Promise<Promise<T>>. But what if you write this?

interface Thenable {
  then(): any;
}

async function foo(): Thenable {
  return {} as any;
}

Does this mean foo returns a Thenable, or a Promise<Thenable> ? What if you wrote this?

type MyTaggedPromise<T> = Promise<T> & { _isCool?: boolean };
async function bar(): MyTaggedPromise<string> {
  // ...

Is that a Promise<MyTaggedPromise<string>> or a MyTaggedPromise<string> ?

@jakubnavratil
Copy link
Author

jakubnavratil commented Mar 19, 2021

I based this on the fact, that you CANNOT return anything else but Promise.
Or am I missing something?

image

I understand that async function returns Promise no matter what (... javascript wraps returned value into Promise)

@RyanCavanaugh
Copy link
Member

I based this on the fact, that you CANNOT return anything else but Promise.
I understand that async function returns Promise no matter what (... javascript wraps returned value into Promise)

Right, and both Thenable and MyTaggedPromise are assignable from Promise, so if we allow anything to be written in a return type position, the question is then "When does that type get auto-wrapped in a Promise<>" ? What I'm showing is that the answer to that question is not at all obivous.

@jakubnavratil
Copy link
Author

When does that type get auto-wrapped in a Promise<>

Everytime. Because you have to write that now like that everytime anyway.
Meaning

async function foo(): Thenable => Promise<Thenable>
async function foo(): MyTaggedPromise<string> => Promise<MyTaggedPromise<string>>

That is the point, you have to wrap it into Promise manually everytime - no exception - so why this can't be done automaticaly?

@RyanCavanaugh
Copy link
Member

So let's say that's allowed

type Baz = number | Promise<string>;
async function foo(): Baz{
    return 0;
}

Is this legal, or not? There are strong arguments either way:

  • Clearly foo is returning the Promise<string> branch of Baz, so returning a number qua Promise<number> is illegal. I wrote Baz meaning "the return type of a synchronous function that returns a number or an async function returning a string".
  • Clearly foo actually returns Promise<number | Promise<string>>, so returning a number is valid. That's what auto-wrapping means.

@jakubnavratil
Copy link
Author

I wrote Baz meaning "the return type of a synchronous function that returns a number or an async function returning a string".

This is ok in non-async land, but nothing stops you writing Promise<Baz> which translates to Promise<number | string> and you already lost the meaning. Which is what you must do manually anyway if you want to use async keyword.

My point being: nothing changes. From outside world, async function foo(): Baz still returns Promise.
To me it looks like your "two ways" can be split between:

  • I can use async keyword
  • I can NOT use async keyword

Because with async, I have Promise everytime so there is no 'branching'.

Consider current behavior:

type Baz = number | Promise<string>;
async function bar(): Promise<Baz> {
  const res: Baz = 1;
  return res;
}

const a: Baz = await foo(); // error: Type 'string | number' is not assignable to type 'Baz'
const b: Baz = foo(); // error: Type 'Promise<Baz>' is not assignable to type 'Baz'.

interface A {
  foo(): Baz;
}
class B implements A {
  async foo(): Promise<Baz> { // error: Property 'foo' in type 'B' is not assignable to the same property in base type 'A'.
    return 1;
  }
}

So it looks to me like you can't use this anyway with async. Or I still lack of imagination.

But if you dont use that strange type and dont try to use it with async function, it is actually smoother from the code point of view. You hide the Promise<> type from code just by using async:

type Baz = number | string;
async function bar(): Baz {
  const res: Baz = 1;
  return res;
}
const a: Baz = await foo();

Also both variants would be valid and produce same type:

async function bar(): Baz // => Promise<number | string>
async function bar(): Promise<Baz> // Promise<number | string>

To me it seems like auto-wrapping is only possible solution. Or is there scenario where auto-wrapping can't be used but can with manual wraping?

Also note that type Baz = number | Promise<string>; ... Promise<Baz> automaticaly unwraps into Promise<number | string>. So if there can be auto-unwrapping, why not auto-wrapping, especially if I had no choise but manually wrap it when using async.

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Mar 19, 2021

This is the bug report I'm foreseeing:

type Baz = number | Promise<string>;
async function foo(): Baz {
    return 0;
}

TypeScript failed to catch a bug here; the only legal thing that I can return from this function is a Promise<string>. That's plain from the function signature; the leading async is only an implementation modifier and shouldn't have spooky action at a distance on the return type. TypeScript shouldn't do this "auto-wrapping" magic; if I wanted to return a Promise<number | string> that's what I would have written. How do I opt-out of this behavior where a return type annotation doesn't mean what it says? I can't even tell from a function declaration whether the return type is getting auto-wrapped or not. In fact, I tried to write

async function foo(): Baz {
    return 0 as unknown as Baz;
}

and it tells me that "Type Baz isn't assignable to type Promise<Baz>" - but that's not what I wanted! I want just Baz but there's no way for me to say that?

@jakubnavratil
Copy link
Author

That is all base on thing that

the leading async is only an implementation modifier and shouldn't have spooky action at a distance on the return type.

Current behavior is somehow magic too, and noone questions it:

async function foo(): Promise<string> {
  // many lines ...
  return 'text'; // return type is not Promise<string>
}

It is okay just because I used async at far distance before. So eitherway async keyword have some power and forces type inconsistency.

But this fails:

function foo(): Promise<string> {
  return 'text'; // error: Type 'string' is not assignable to type 'Promise<string>'
}

It just bugs me that writing async is not enough and I have to write Promise<> manually anyway. To me not using Promise wrappers in return types is just cleaner code.

Also, hypoteticaly, if we would introduce throws (#13219) on function declarations and update Promise rejection type, declarations would be more consistent.

function foo(): number throws CustomError
async function foo(): number throws CustomError // => Promise<number, CustomError>

@typescript-bot
Copy link
Collaborator

This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@Pyrolistical
Copy link

I wrote Baz meaning "the return type of a synchronous function that returns a number or an async function returning a string".

@RyanCavanaugh I think the async keyword always autowrapping makes sense, and we can achieve your desired maybe async Baz but not using the async keyword. This is identical on how ES async works.

maybe sync Baz:

type Baz = number | Promise<string>;
function foo(): Baz {
    if(sometimes()) {
      return 0;
    } else {
      return Promise.resolve('hello');
    }
}

const result = await foo();
// result is 0 or 'hello'

Then we can reserve async function foo(): Baz to be the same as function foo(): Promise<Baz>, which is the same as function foo(): Promise<number | Promise<string>>.

async Baz:

type Baz = number | Promise<string>;
async function foo(): Baz {
    if(sometimes()) {
      return 0;
    } else {
      return Promise.resolve('hello');
    }
}

const result = await foo();
// result is 0 or 'hello'

Since Promises collapse, we get 'hello' and not a Promise of 'hello'.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

5 participants