You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Suggestion: Implied Promise return type in async functions #7284
Apologies if this has already been brought up, but I wasn't able to find any issues that cover this topic (#5911 seems closest).
The Current State
Currently, if you want to enforce typing of an async function's resolved value, you must declare a return type of :Promise<ResolveType>:
asyncfunctionmakeStuff():Promise<Stuff> {}
This is a bit counterintuitive when it comes to async functions, as they generally mask the fact that there's a Promise there to begin with. If you are utilizing promises purely via async/await, you're never going to see the token Promise in your code.
Proposal
When providing the return type of an async function, implicitly wrap it in a Promise<T> (unless it is an explicit Promise<T> type expression?). This seems reasonable now that #6631 is in.
asyncfunctionmakeStuff():Stuff {}
Seems like the least surprising way of declaring an async function
I also think it is annoying to have to specify the Promise type, when it can inferred from the async keyword already.
Tingan Ho (@tinganho) the whole Promise<T> return type can be inferred already without having to specify a return type annotation at all. I take it you are referring to a situation where the compiler cannot work out from the function body what is the T type in Promise<T>, so that you have to annotate it yourself? How common is this?
Troy Gerwien (@yortus) sorry my bad, i was referring to the original proposal that Promise is not required to annotate a return type for an async function. There is an implied Promise when you use the async keyword.
I think a fuller example showing the pain point / motivating scenario would be useful. The OP gave this example:
asyncfunctionmakeStuff():Stuff{}
But it's unclear from this line why the : Stuff annotation needs to be added at all. What does the function body look like, that the compiler cannot infer the return type? Usually it can accurately infer the return type with no annotation at all.
And on the point of return type inference, Ian MacLeod (@nevir) what do you propose the language service should do with this? E.g. if in VS Code I hover over the makeStuff identifier, does it pop up function makeStuff(): Promise<Stuff> like at present, or something else?
But it's unclear from this line why the : Stuff annotation needs to be added at all. What does the function body look like, that the compiler cannot infer the return type? Usually it can accurately infer the return type with no annotation at all.
It is good practice to specify the intent first and then implement the body. If you intend the type to be of some type T, then the checker enforces the function body to return that type. I believe specifying intention before implementation yields overall less buggy result.
And in this case my intention of a function is to be async and have a return type of T. But I think it is redundant to have to specify that the return type is Promise<T>, when we have already provided the async keyword.
It is good practice to specify the intent first and then implement the body
Interesting.... I agree with this statement but draw a rather different conclusion. I like to express my intent through clear interfaces that can be publicly shared with callers, and then provide an implementation that satisfies these interfaces without callers needing to know the implementation details.
In this light, using an async function is often an implementation detail - it's one of several possible ways to implement a function that returns a promise.
For example, I may provide some public API function that returns a 'thenable', without wanting to couple callers to the specific promise implementation, which might be bluebird for some builds and ES6 Promise for others. So I'd like my public API to look like this:
exportclassFoo{makeStuff(): Thenable<Stuff>;}
Now I can implement this class in a variety of ways that satisfy the contract, keeping the implementation details decoupled from the public interface.
One such way would be like this:
exportclassFoo{asyncmakeStuff(): Thenable<Stuff>{// make some stuff...// returns an ES6 Promise of stuff, which is-a Thenable<Stuff>}}
Either way, the public interface remains the same.
The declaration async makeStuff(): Thenable<Stuff> would not be possible under this proposal. In fact, it's already not possible due to the already divergent treatment of async function return type annotations. I'd prefer that treatment of async function return type annotations was made more consistent with how they currently work on ordinary and generator functions, e.g. so the async function implementation in the above example works just as well as the bluebird implementation.
Does the first one mean that the interface only matches an async function, and not an ordinary function that returns a promise? Putting async in an interface declaration adds no expressivity to the type system, but possibly introduces an artificial distinction between two kinds of promise-returning functions that are interchangeable (and for most purposes indistinguishable) at runtime.
Actually, that's not a valid signature - async isn't allowed as part of a type
I think this proposal is to make it valid. Otherwise it is a little bit inconsistent that you can write async with implied Promise on function declarations, but not in interface declarations. Or do you mean that it is not technically possible?
Since Promises can't nest (i.e. you can't have a Promise<Promise<T>> like you can have a Task<Task<T>> in C#), it does reduce the importance of having the Promise<> in the return type.
In both cases, declaring the type instead of letting it be inferred provides extra security. Both make calls to other functions w/ similar contracts, and you would want to know if they modify their signature (say during a refactor, etc)
⚠️ This is a dangerous proposal as it would interact badly with possible type system enhancements.
If this proposal was accepted, it would be impossible to declare an async method as returning Promise<number> | number. This is not accepted by the compiler today, but desirable (see my comment here: #6686 (comment)).
Fundamentally, this is lying about the return type of the function, which can clash with more complex type declarations. I don't think that saving nine characters Promise<> is a good enough motivation.
If you're keen on saving characters you often can let TS infer the actual return type of your method and type nothing at all.
If you are utilizing promises purely via async / await, you're never going to see the token Promise in your code.
That is not a good argument because it is not true.
You might still want to use Promise.all() or Promise.race().
You might still construct some promises using the ctor new Promise((resolve, reject) => {}). There are things that you can't do without that, in particular wrapping promises around async code that is based on callbacks. This can be your own code, e.g. if you wrap some user interactions inside Promises.
You might still need to type abstract functions or interface members, which require Promise for asynchronous API. This use case would be slightly confusing for less experienced devs, as an interface declaring Promise<T> would be implemented by an async methods declared as T.
IntelliSense would still show you Promise<T> as the return type of many functions (yours and 3rd party libraries). Again that could be confusing to see a different type from the declaration.
Ian MacLeod (@nevir) Today the compiler does not allow this type annotation, but did you read the comment I linked next to that remark?
That other issue #6686 is about allowing such annotations and my comment provides a motivation for that.
I understand that, but it's not something that TypeScript can control. If you want to change that behavior, you really should be bringing it up with TC39, as it will affect all JavaScript based platforms, not just TypeScript.
At some point, the various vendors will implement this natively (and probably will start doing so relatively soon, since it's a stage 3 proposal). IMO It would be disastrous for TypeScript's async/await to behave differently from the native implementation.
Ian MacLeod (@nevir) I think you did not get it right.
It is not about changing what an async method actually returns. It will always be a promise.
It is about the return type annotation, which is a TS thing.
It would be good to be able to say that an async method returns a union type that includes a promise. For example Promise<T> | T.
That does not change the fact that the async function always returns Promise<T>, but it is a valid return type declaration.
It is useful because of interfaces and inheritance. For instance, you may want to allow a derived class to have a synchronous implementation. There is a real world example in the linked issue.
Ian MacLeod (@nevir) just to be clear, jods (@jods4) is fully aware that async functions always return a promise and is not suggesting otherwise. There is a clear distinction between the actual return type and the return type annotation, the latter being completely in TypeScript's control.
In TypeScript, return type annotations just have to be assignment-compatible. E.g., function self(): {length: number} { return self; } is valid. This clearly always returns a function. The return type anntotation doesn't change or contradict that, but only exposes a small assignment-compatible part of the return type.
His example shows a practical benefit of this distinction, where a base class method is always async, but allows derived classes to override this method with a sync implementation, and it all passes type-checks.
Here is a practical example. Suppose we maintain a library that uses promises in it's public API.
But we don't want our library users to depend on the specific Promises/A+ implementation we use to implement the library, so that we can change the implementation later, say from bluebird promises to native async/await, or so that we can support polyfilling in browsers, etc.
If our API specifically exposes say bluebird promises or es6 promises, then our users may start
writing things like p.finally(...) or if (p instanceof Promise) {...}, which only work with a specific promise type. Then if we change the promise implementation, we will break client code.
So instead, we write a generic interface Thenable covering only the interoperable part of Promises/A+, and make the public API use that interface instead, like so:
Problem solved! Now our library users just see Thenables, and don't depend on the specific promise implementation, which we are free to change in future versions or in browser builds etc.
Later, let's say we want to update getBar's implementation to use async/await, still keeping the public API strictly about Thenables.
This implementation clearly always returns an es6 promise, but our library users see the same Thenable-based interface as before, which is exactly what we want.
And that's where we get to current TypeScript and this proposal:
current TypeScript allows assignment-compatible return type annotations everywhere except async functions. That already means the above update of getBar doesn't compile. Some of us are asking the team to reconsider this and make async functions consistent with the rest of the type system in this regard.
the current proposal, while saving some keystrokes in a few places, would also make the above example impossible, and that's the point jods (@jods4) is making.
Discussed and basically came to the same conclusions as jods (@jods4) and Troy Gerwien (@yortus). Apparently the people working on async thought of doing this originally as well and decided it'd be a bad idea. There's also the fundamental tenet that a return type annotation shouldn't depend on modifiers on the other side of the screen to be interpreted correctly.
Activity
yortus commentedon Feb 29, 2016
Would you suggest doing the same for generator functions for consistency:
Also, I think a case could be made for this change being equally confusing, even as you say when the
Promisetoken appears nowhere in the source:nevir commentedon Feb 29, 2016
I think that would make sense; though I haven't wrapped my head around generators enough to be confident.
That's a good error to have! Lets you know that you should be
awaitingbar(). Ideally the error message would hint aboutawait, as well.tinganho commentedon Feb 29, 2016
I also think it is annoying to have to specify the
Promisetype, when it can be inferred from theasynckeyword already.ljqx commentedon Feb 29, 2016
Then how could we specify that we want to use Bluebird's
Promise, not the original one?nevir commentedon Feb 29, 2016
As of #6631, you can't do that any more (except by providing your own
__awaiterimplementation)yortus commentedon Feb 29, 2016
Tingan Ho (@tinganho) the whole
Promise<T>return type can be inferred already without having to specify a return type annotation at all. I take it you are referring to a situation where the compiler cannot work out from the function body what is theTtype inPromise<T>, so that you have to annotate it yourself? How common is this?tinganho commentedon Feb 29, 2016
Troy Gerwien (@yortus) sorry my bad, i was referring to the original proposal that
Promiseis not required to annotate a return type for an async function. There is an impliedPromisewhen you use the async keyword.yortus commentedon Feb 29, 2016
I think a fuller example showing the pain point / motivating scenario would be useful. The OP gave this example:
But it's unclear from this line why the
: Stuffannotation needs to be added at all. What does the function body look like, that the compiler cannot infer the return type? Usually it can accurately infer the return type with no annotation at all.And on the point of return type inference, Ian MacLeod (@nevir) what do you propose the language service should do with this? E.g. if in VS Code I hover over the
makeStuffidentifier, does it pop upfunction makeStuff(): Promise<Stuff>like at present, or something else?tinganho commentedon Feb 29, 2016
It is good practice to specify the intent first and then implement the body. If you intend the type to be of some type
T, then the checker enforces the function body to return that type. I believe specifying intention before implementation yields overall less buggy result.And in this case my intention of a function is to be async and have a return type of
T. But I think it is redundant to have to specify that the return type isPromise<T>, when we have already provided the async keyword.yortus commentedon Feb 29, 2016
Interesting.... I agree with this statement but draw a rather different conclusion. I like to express my intent through clear interfaces that can be publicly shared with callers, and then provide an implementation that satisfies these interfaces without callers needing to know the implementation details.
In this light, using an async function is often an implementation detail - it's one of several possible ways to implement a function that returns a promise.
For example, I may provide some public API function that returns a 'thenable', without wanting to couple callers to the specific promise implementation, which might be
bluebirdfor some builds and ES6Promisefor others. So I'd like my public API to look like this:Now I can implement this class in a variety of ways that satisfy the contract, keeping the implementation details decoupled from the public interface.
One such way would be like this:
Another way would be like this:
Either way, the public interface remains the same.
The declaration
async makeStuff(): Thenable<Stuff>would not be possible under this proposal. In fact, it's already not possible due to the already divergent treatment of async function return type annotations. I'd prefer that treatment of async function return type annotations was made more consistent with how they currently work on ordinary and generator functions, e.g. so theasync functionimplementation in the above example works just as well as the bluebird implementation.tinganho commentedon Feb 29, 2016
Ian MacLeod (@nevir) I think your proposal misses how interface declarations should look like but I guess they should use the async keyword as well?
yortus commentedon Feb 29, 2016
How is this:
different from this:
Does the first one mean that the interface only matches an
async function, and not an ordinary function that returns a promise? Puttingasyncin an interface declaration adds no expressivity to the type system, but possibly introduces an artificial distinction between two kinds of promise-returning functions that are interchangeable (and for most purposes indistinguishable) at runtime.tinganho commentedon Feb 29, 2016
Troy Gerwien (@yortus) my thought was that there is no difference between them.
DanielRosenwasser commentedon Feb 29, 2016
Actually, that's not a valid signature -
asyncisn't allowed as part of a type (since all you need to know is that there's promise returned).tinganho commentedon Feb 29, 2016
I think this proposal is to make it valid. Otherwise it is a little bit inconsistent that you can write async with implied
Promiseon function declarations, but not in interface declarations. Or do you mean that it is not technically possible?4 remaining items
Arnavion commentedon Feb 29, 2016
Since Promises can't nest (i.e. you can't have a
Promise<Promise<T>>like you can have aTask<Task<T>>in C#), it does reduce the importance of having thePromise<>in the return type.yortus commentedon Mar 1, 2016
Ian MacLeod (@nevir) do you have a motivating example with code?
nevir commentedon Mar 1, 2016
I don't have any I can share, unfortunately. But here's an example from around the community:
In both cases, declaring the type instead of letting it be inferred provides extra security. Both make calls to other functions w/ similar contracts, and you would want to know if they modify their signature (say during a refactor, etc)
jods4 commentedon Mar 5, 2016
If this proposal was accepted, it would be impossible to declare an async method as returning
Promise<number> | number. This is not accepted by the compiler today, but desirable (see my comment here: #6686 (comment)).Fundamentally, this is lying about the return type of the function, which can clash with more complex type declarations. I don't think that saving nine characters
Promise<>is a good enough motivation.If you're keen on saving characters you often can let TS infer the actual return type of your method and type nothing at all.
That is not a good argument because it is not true.
You might still want to use
Promise.all()orPromise.race().You might still construct some promises using the ctor
new Promise((resolve, reject) => {}). There are things that you can't do without that, in particular wrapping promises around async code that is based on callbacks. This can be your own code, e.g. if you wrap some user interactions inside Promises.You might still need to type
abstractfunctions orinterfacemembers, which requirePromisefor asynchronous API. This use case would be slightly confusing for less experienced devs, as an interface declaringPromise<T>would be implemented by an async methods declared asT.IntelliSense would still show you
Promise<T>as the return type of many functions (yours and 3rd party libraries). Again that could be confusing to see a different type from the declaration.nevir commentedon Mar 5, 2016
It is my understanding that an
asyncfunction must return aPromise; it would be a compiler error not to.At least, that's how I'm interpreting section 1.1.8 of the
async/awaitspec. All conditions of EvaluateBody return aPromise. This is also the current behavior of both TypeScript and Babel.I.e.
jods4 commentedon Mar 5, 2016
Ian MacLeod (@nevir) Today the compiler does not allow this type annotation, but did you read the comment I linked next to that remark?
That other issue #6686 is about allowing such annotations and my comment provides a motivation for that.
nevir commentedon Mar 6, 2016
I understand that, but it's not something that TypeScript can control. If you want to change that behavior, you really should be bringing it up with TC39, as it will affect all JavaScript based platforms, not just TypeScript.
At some point, the various vendors will implement this natively (and probably will start doing so relatively soon, since it's a stage 3 proposal). IMO It would be disastrous for TypeScript's
async/awaitto behave differently from the native implementation.The
async/awaitspec's repo would be a better place to bring it up.jods4 commentedon Mar 6, 2016
Ian MacLeod (@nevir) I think you did not get it right.
It is not about changing what an async method actually returns. It will always be a promise.
It is about the return type annotation, which is a TS thing.
It would be good to be able to say that an async method returns a union type that includes a promise. For example
Promise<T> | T.That does not change the fact that the
asyncfunction always returnsPromise<T>, but it is a valid return type declaration.It is useful because of interfaces and inheritance. For instance, you may want to allow a derived class to have a synchronous implementation. There is a real world example in the linked issue.
yortus commentedon Mar 6, 2016
Ian MacLeod (@nevir) just to be clear, jods (@jods4) is fully aware that async functions always return a promise and is not suggesting otherwise. There is a clear distinction between the actual return type and the return type annotation, the latter being completely in TypeScript's control.
In TypeScript, return type annotations just have to be assignment-compatible. E.g.,
function self(): {length: number} { return self; }is valid. This clearly always returns a function. The return type anntotation doesn't change or contradict that, but only exposes a small assignment-compatible part of the return type.His example shows a practical benefit of this distinction, where a base class method is always async, but allows derived classes to override this method with a sync implementation, and it all passes type-checks.
Here is a practical example. Suppose we maintain a library that uses promises in it's public API.
But we don't want our library users to depend on the specific Promises/A+ implementation we use to implement the library, so that we can change the implementation later, say from bluebird promises to native async/await, or so that we can support polyfilling in browsers, etc.
If our API specifically exposes say bluebird promises or es6 promises, then our users may start
writing things like
p.finally(...)orif (p instanceof Promise) {...}, which only work with a specific promise type. Then if we change the promise implementation, we will break client code.So instead, we write a generic interface
Thenablecovering only the interoperable part of Promises/A+, and make the public API use that interface instead, like so:Problem solved! Now our library users just see
Thenables, and don't depend on the specific promise implementation, which we are free to change in future versions or in browser builds etc.Later, let's say we want to update
getBar's implementation to useasync/await, still keeping the public API strictly aboutThenables.The obvious approach would be:
This implementation clearly always returns an es6 promise, but our library users see the same
Thenable-based interface as before, which is exactly what we want.And that's where we get to current TypeScript and this proposal:
getBardoesn't compile. Some of us are asking the team to reconsider this and make async functions consistent with the rest of the type system in this regard.RyanCavanaugh commentedon May 9, 2016
Discussed and basically came to the same conclusions as jods (@jods4) and Troy Gerwien (@yortus). Apparently the people working on async thought of doing this originally as well and decided it'd be a bad idea. There's also the fundamental tenet that a return type annotation shouldn't depend on modifiers on the other side of the screen to be interpreted correctly.