async function delay(millis: number): PromiseLike<void> {
return Promise.resolve(undefined);
}
The code above is not valid and is rejected by the compiler.
Could you explain why a PromiseLike is not sufficient as a return value. I looked at the generated code and I couldn't spot an obvious reason.
if you use a type annotation, it is going to be used to create the promise object wrapping the return value. there is no "value" named PromiseLike, so it is going to fail at runtime.
so either do not give it a type annotation, and it would be inferred to be the built-in "Promise", or use a Promise library.
Also, since you're returning a promise and not the value, you don't need to mark it as async anyway.
This might get complicated if you use two libraries which use different promise implementation. Then the code needs to wrap them.
I still think that the code should allow PromiseLike.The awaiter code the compiler generate could still create an instance of a Promise since it conforms to PromiseLike. However the cast method must not do an instanceof Promise check. Instead it would need to check for a then function. All the awaiter code needs right now is a then function.
@dbaeumer if you want to annotate the return value as PromiseLike<T>, then just return a value assignable to type T from the async function and it works fine. E.g.:
async function f1(): PromiseLike<void> {
/* ...some async operation... */
return;
}
async function f2(a: number, b: number): PromiseLike<number> {
/* ...some async operation... */
return a + b;
}
async function f3(): PromiseLike<string> {
/* ...some async operation... */
return 'foo';
}
EDIT: Ahhh, I think I see the error you are talking about now:
Type PromiseLike<...> is not a valid async function return type
I tend to agree - the compiler is being too strict here. PromiseLike<any> is a valid annotation for any async function return value, because whatever promise instance is returned is sure to be assignable to it.
@Arnavion shorten my example to much. I need the async keyword since I want to use await in the body. Something like:
declare function use(): PromiseLike<number>;
async function delayer(millis: number): Promise<void> {
let result = await use();
return Promise.resolve(undefined);
}
@dbaeumer but why do you need the line return Promise.resolve(undefined);? Why not just allow the async body to return - it will wrap the return value in a promise - you don't need to create the promise explicitly. i.e.:
async function delayer(millis: number) {
await use();
}
That will return Promise<void>, no need for any annotations or creating explicit promises.
Is this what you are trying to do:
async function delayer(millis) {
await new Promise(resolve => setTimeout(resolve, millis));
}
That will return a Promise<void> that resolves after millis milliseconds, and the resolution value will be undefined.
@yortus the example is not about implementing a delayer :-). Here is what I want to do: I have a library that provides APi to others. This API is speced in terms of PromiseLike. Now I want to asyncfy the library. To do so I need to change all signatures to return Promise instead of PromiseLike otherwise I can't use await inside the implementation. This is still doable. What I don't fully understand what will happen if I have code like this:
function imported(): PromiseLike<string> {
// returns a Promise that is not the 'native' Promise implementation in node.
}
async function wrap() {
return imported();
}
async function use {
var value = await wrap();
console.log(value);
}
Will this always print a string value or could it happen to be the Promise type returned by imported(). The awaiter code does some instanceof Promise checks. If it always prints the strings I think it would be more clear if async functions return a PromiseLike
I tested it with the Promise implementation form here https://github.com/then/promise and it returned a string.
@dbaeumer if you do it like this it should always work regardless of promise implementation:
function imported(): PromiseLike<string> {
// returns a Promise that is not the 'native' Promise implementation in node.
}
async function wrap() {
return await imported(); // 'unwraps' the PromiseLike and 're-wraps' as native promise
}
async function use {
var value = await wrap();
console.log(value);
}
@dbaeumer to add to the above, the instanceof checks in the awaiter should only relate to return values inside async functions, so that if a Promise<T> is returned it does not get wrapped into a Promise<Promise<T>>.
But the await operator should work with any A+ thenable regardless of what its specific implementation is. So return await x works with any thenable x.
@yortus I know, but this might be error prone. And in C# I can always return the called async method without await which is very nice (and to my knowledge save some CPU cycles)
@dbaeumer Isn't it possible to just await a function returning a promise (no async marker)?
function delay(t: number): PromiseLike<void> {
return new Promise<void>(resolve => {
setTimeout(resolve, t);
});
}
await delay(1000);
@dbaeumer Awaiting an A+ promiselike won't be error prone. If you're worried about the performance difference between return x and return await x, benchmark it and see. Given that async functions are downleveled to ES6 generators which V8 can't optimise, I think you'll find any performance difference very minor next to the slowdown due to the generator body not being optimised.
@yortus what I meant with error prone is that people might forget it (especially if they come from a C# background).
@dbaeumer if you are suggesting that the awaiter implementation should get rid of the instanceof checks on the return value and just check if it's something thenable, that might make sense. But I don't know if it's been drafted that way in the ES7 spec for some technical reason. I think @rbuckton would be the one to ask.
EDIT: I should have looked at the awaiter helper function first. Made a more informed comment below.
@dbaeumer I just took a look at the awaiter function, and you can definitely rely on return x to work the way you expect even if x is a non-native promise. The instanceof checks are just used to cast all awaited expressions to native promises before calling then on them.
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promise, generator) {
return new Promise(function (resolve, reject) {
generator = generator.call(thisArg, _arguments);
function cast(value) { return value instanceof Promise && value.constructor === Promise ? value : new Promise(function (resolve) { resolve(value); }); }
function onfulfill(value) { try { step("next", value); } catch (e) { reject(e); } }
function onreject(value) { try { step("throw", value); } catch (e) { reject(e); } }
function step(verb, value) {
var result = generator[verb](value);
result.done ? resolve(result.value) : cast(result.value).then(onfulfill, onreject);
}
step("next", void 0);
});
};
In the awaiter code, the return value of the async function is passed directly to the resolve function of the async function's promise. The Promises A+ resolution procedure, which ES6 promises adhere to, ensures that a promise resolved with any thenable will resolve to that thenable's eventual value.
So you can definitely rely on the behaviour of return x you are talking about. x can be a non-native promise and it will never be 'double-wrapped'. As for the compiler complaining about the PromiseLike annotation on an async function, I think that's an unnecessary restriction that the compiler could relax.
@jrieken await can only be used inside the body of an async function.
yeah, but being in an async-function isn't hard, right
@jrieken yeah probably not the hardest part of the problem :-)
I think you already got the answer to this, but just to be sure...
This might get complicated if you use two libraries which use different promise implementation. Then the code needs to wrap them.
However the cast method must not do an instanceof Promise check. Instead it would need to check for a then function. All the awaiter code needs right now is a then function.
The cast() function in the compiler-generated __awaiter is actually superfluous. It could simply do Promise.resolve() new Promise(resolve => resolve(value)) and everything would work the same, since Promise resolver will also check whether the value is a Promise or a thenable. (The former is an optimization of the latter; the latter is required.)
Coming back to your OP, as @mhegazy said the reason your code is not allowed as-is is that the the return type of an async function is used for the promise constructor inside __awaiter - it's the third parameter. There's nothing which prevents you from using PromiseLike as the return type. You just need to satisfy the compiler's demand that it be usable as __awaiter's third parameter. This compiles just fine:
interface PromiseLike<T> {
then<U>(onFulfilled: (value: T) => U, onRejected: (reason: any) => U): PromiseLike<T>;
}
declare var PromiseLike: {
new<T>(resolver: (resolve: (value: T) => void, reject: (reason: any) => void) => void): PromiseLike<T>;
};
async function delay(millis: number): PromiseLike<void> {
return Promise.resolve(undefined);
}
With a real promise implementation this would be provided by its .d.ts of course.
So I don't think there's any problem?
The __awaiter implementation is designed to support an async function returning any Promise/A+ compatible promise implementation. How you determine the type of Promise to return is through the return type annotation of the async function.
The reason __awaiter has a cast function, and does not rely on Promise.resolve is that Promise/A+ does not specify a Promise.resolve static method, and several Promise/A+ compatible libraries do not implement one. The cast function is designed to act in a fashion similar to the native Promise.resolve, which wraps any promise with a different prototype chain and constructor in an instance of the promise type on which resolve was called.
Please note that the Promise identifier in __awaiter is the on provided as the third argument to the call to __awaiter, which will be the constructor function referenced in the return type annotation:
async function fn(): MyPromise<number> {
return 1;
}
becomes:
function fn() {
return __awaiter(this, void 0, MyPromise, function* () {
return 1;
});
}
In this case, MyPromise is the value of Promise inside the __awaiter helper. If MyPromise is a non-native, Promise/A+ -compatible implementation _without_ a static resolve method, then __awaiter will still be able to function as expected.
This approach was chosen to allow you to "Bring your own Promise" to async functions, whether that is a non-native promise implementation from a library, the native Promise implementation, or a subclass of a native Promise.
The reason PromiseLike cannot be used here, is that PromiseLike is a type-only declaration and cannot be expressed as a value at runtime. If you had:
async function delay(millis: number): PromiseLike<void> {
}
How would TypeScript know what kind of Promise to create here? We cannot rely on type inference for a return value here, as the value for some return types cannot be reached at the moment the function is called. That type may come from a call to a function defined in another module.
Instead, we can rely on the type specified in the return type annotation of the async function as you must be able to reference it in the same file (either through a global reference or an import), and therefore we can verify that we can express it as a value.
@Arnavion cast is not superfluous, it is part of the specified resolution procedure for await in the official specification. It also reduces the number of Promise allocations in instances where it is not needed.
Please note that return x and return await x in an async function are functionally similar, though return await x has slightly more overhead as it requires an additional step of the generator. The reason is that regardless of whether the return value of an async function is a promise or a non-promise value, it will be wrapped by the outer promise created by the async function.
@Arnavion: While your sample compiles fine, when you execute it at runtime you will get a TypeError since PromiseLike is undefined. The type must resolve to a valid constructor function at runtime, so that it can be created via new.
castis not superfluous, it is part of the specified resolution procedure for await in the official specification. It also reduces the number of Promise allocations in instances where it is not needed.
This is what I meant by superfluous, that it's an optimization. Note that the spec uses NPC::resolve for the return/awaited value which also does not do constructor check, and is thus the same as unconditionally calling the constructor and using resolver.resolve with the value, not Promise.resolve(). (Only Promise.resolve does the constructor-check for the value.) Edit: And yes, I'm not saying the optimization is wrong and shouldn't be used. There was previous discussion that the cast() function needed modification, to which I was responding that it would be unnecessary to change it.
While your sample compiles fine, when you execute it at runtime you will get a TypeError since PromiseLike is undefined. The type must resolve to a valid constructor function at runtime, so that it can be created via new.
Of course. The point was that the OP's code is valid. They just didn't provide a constructor for their PromiseLike, and it's not that the compiler only supports Promise as the return type.
@Arnavion: After reviewing the latest version of the spec, you are correct that there will always be an allocation of a new promise. I will look into changing __awaiter to be more in-line with the current proposal.
Here's a possible shorter alternative that I may use:
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new P(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator.throw(value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.call(thisArg, _arguments)).next());
});
};
I've also changed the promise constructor argument from Promise to P so it is less likely to be confused with the native Promise constructor (I'm using an uppercase value here as some JS linters complain about using new with a non-uppercased function).
@rbuckton we should file an issue to track this modification.
@mhegazy, @Arnavion: I've filed #5941 to track this issue. We can continue any related discussion in that issue.
@Arnavion: I don't want to further confuse the topic. The lib.d.ts file already defines a PromiseLike (and PromiseConstructorLike) interface that the compiler uses to verify that a promise implementation is consistent with the needs of the __awaiter helper. I would not recommend co-opting PromiseLike for other uses, and I don't believe that was @dbaeumer's intent when he filed the issue.
@rbuckton I guess this issue does bring up the surprise factor that return type annotations behave differently for async functions than they do everywhere else in TypeScript, since they are used to pass a _value_ to the awaiter and cannot be just a _type_ as elsewhere.
For example I found it unintuitive that the following is an error, given the way annotations work elsewhere:
async function bar(): any { // Error: Type 'any' is not a valid async function return type
}
I think that also captures the surprise that PromiseLike can't be used as a return type annotation.
Given that 'bring your own Promise implementation' could be achieved by just passing the value of Promise that is in scope when the __awaiter(...) call is made, it seems an interesting choice to diverge from how annotations otherwise behave. Was there some reason to do it this way rather than just using whatever value of Promise is in scope?
@rbuckton When in future async/await is passed through as-is _without_ downleveling to ES6, what will the 'bring your own Promise implementation' story be like then? I can't see how the return type annotation could be used in that case?
This aligns somewhat with the C# implementation of async/await in that an async function must return Task, Task<T>, or void (though we cannot ensure the same semantics as C# for async...void). It also allows you to return a Promise subclass from async function.
Requiring a developer to alias their type as Promise is a high bar for entry, and prevents use cases where you might want to comingle both native Promises and subclasses in the same file.
We could consider falling back to Promise for compatible type-only declarations, but I can already think of a few corner cases where that could cause issues with a user's expectation versus what we emit. The current approach is more restrictive, which allows us to consider ways to relax these restrictions in the future without causing breaking changes.
@yortus: We can still support it by nesting the async function when necessary. Being able to supply the type of Promise to use for an async function is something I will discuss with the TC39 champion for async/functions.
Requiring a developer to alias their type as Promise is a high bar for entry, and prevents use cases where you might want to comingle both native Promises and subclasses in the same file.
Not really, it just has to be in scope when __awaiter is called, not necessarily the whole file. It's not ideal, but neither is using the return type annotation as a value. I was just curious about the trade-off.
We can still support it by nesting the async function when necessary.
Just wondering what would the emit look like? Do you mean something like this:
// file: foo.ts
async function foo(): MyPromise
//...
}
// file: foo.js
function foo() {
return new MyPromise(function (resolve, reject) {
foo_1().then(resolve, reject);
});
async function foo_1()
//...
}
}
@rbuckton thanks a lot for the very good explanation how async and await works under the hood. Now that I understand it, it makes sense to me and the current implementation covers all my needs. Especially the one that we don't want to force a promise library onto others when implementing interfaces we spec.
However I would have never found out myself that the return type of a async function determines the Promise to use. In that regard I agree with @yortus. May be it would be helpful to extend the error message with something like this: Type 'xxx' is not a valid async function return type. Valid return types are Promise/A+ compatible implementations.
+1 for better error message.
How about adding "everything you thought you knew about type annotations is wrong!"
Only half joking, since this feature uses type annotation syntax but behaves very differently.
@yortus the behavior here is consistent with other parts in the system, e.g. generators, and is consistent with use of other promise libraries and other languages. i know you filed #5945, but i do not see any new design points that were not available at the time this design decision was made, let a side being a breaking change.
the behavior here is consistent with other parts in the system, e.g. generators
@mhegazy can you clarify what you mean by this?
interface SomeIterator {
next: Function;
}
interface SomePromise {
then: Function;
}
function* gf(): SomeIterator { } // OK
async function af(): SomePromise { } // ERROR
How is this consistent?
More examples:
interface SomeIterator {
next: Function;
}
interface SomePromise {
then: Function;
}
// Generator functions
var genFuncRet1: SomeIterator = (function* () {})(); // OK: IterableIterator<T> is assignable to SomeIterator
var genFuncRet2: any = (function* () {})(); // OK: IterableIterator<T> is assignable to any (duh!)
function* genFunc1(): SomeIterator { } // OK: IterableIterator<T> is assignable to SomeIterator
function* genFunc2(): any { } // OK: IterableIterator<T> is assignable to any (duh!)
// Async functions
var asyncFuncRet1: SomePromise = (async function () {})(); // OK: Promise<T> is assignable to SomePromise
var asyncFuncRet2: any = (async function () {})(); // OK: Promise<T> is assignable to any (duh!)
async function asyncFunc1(): SomePromise { } // ERROR: Wut??
async function asyncFunc2(): any { } // ERROR: Surprise!
The behaviour here is _not_ consistent with other parts of the type system, not even generators. Annotations on generators _describe_ the JavaScript behaviour but don't change it. Annotations on async functions _change_ the JavaScript behaviour, and give compiler errors (like above) when used to describe it.
As you mentioned, a gnerator function return type annoation is not the type of the item it iterates on, rather an iterator. similarly an async function return type annotation is not the type of the promised value, rather a promise.
This is also consistent with the normal type annotation, as the annotation is what the function _returns_, with no exceptions. an async function returns a promise, you can call it without an await and get the promise.
The other instances you reference go back to the down-level/promise library support, which is a unique aspect.
@mhegazy I recognise there are aspects that are consistent with other parts and have no problem with these aspects. Return value annotations must agree with what is actually returned - that's always been the case. But by this logic, PromiseLike should be a valid annotation as the OP expected.
It is the _nature of the inconsistencies_ that alarms me. For example:
// File: foo1.ts
import BluebirdPromise = require('bluebird');
export async function foo() { }
// File: foo2.ts
import BluebirdPromise = require('bluebird');
export async function foo(): BluebirdPromise { }
These two files differ only by a return type annotation. Yet they have different runtime behaviour. This is an important departure for TypeScript from several of (what I thought were) it's key goals. Namely:
Prior to v1.7 anyone interoperating with TypeScript code could rely on the above three statements, since all parts of TypeScript respected them and they are in fact written for all to see in the design guidelines and are widely regarded as a core concept of the language.
Now all three statements are false, with the arrival of this "unique aspect".
It's not simply a "unique aspect" - it's a significant departure from the design goals. Perhaps it seems like an insignificant corner case now, but in a few years when async functions are as widely used as ordinary functions, I think more users will be surprised by the inconsistent semantics of return type annotations and raise similar issues to the one raised by the OP here.
I am not sure i agree with these statements. there is a transformation taking place. the mere fact of putting "async" modifier on a function changes its semantics, and has a clear runtime impact. that is, in my opinion, similar to exporting a function.
Now, to make this transformation work we need a promise constructor. There are a few options:
Each of these approach have its own pros and cons. @rbuckton has outlined some of these earlier. we can discuss this more, but i do not see how either would violate the design goals.
the mere fact of putting "async" modifier on a function changes its semantics
'async' is a JavaScript modifier, not a TypeScript type annotation. Of course it changes the runtime behaviour, that's expected. But can you see that just changing a type annotation and getting different Javascript code and behaviour is something completely new to TypeScript, and is in fact in conflict with statements like _"[don't] emit different code based on the results of the type system."_ (non-goal 5).
A type annotation is a _result of the type system_ - it is not JavaScript - ergo it should not emit different code.
Async function return type annotations (in their current form) are _not erasable_ because that would potentially change runtime behaviour. That is not permitted in a _"consistent, fully erasable, structural type system"_ (goal 9).
BTW regarding the two options:
- a global Promise variable that is assumed to always exist.
- a per function value, extracted from the return type annotation.
The current implementation is doing _both_ of these isn't it? If no annotation is present it passes the identifier Promise to the awaiter function and assumes it exists.
While I'm still adamant that TypeScript should not be emitting different code based on a type annotation, I feel like I'm not getting very far making that case, so can I make a different suggestion?
Would it be possible to alter the current implementation so that it works as the OP expected? That is, if an async function has its return type annotated with 'just a type', not a class (e.g. the PromiseLike interface), then that should _not_ be an error as long as the annotation is type-compatible with Promise, and such a function would return a standard ES6 Promise.
That would mean the following would be valid code:
async function foo(a: PromiseLike<number>, b: PromiseLike<number>): PromiseLike<number> {
return await a + await b;
}
...and calling this function would return an ES6 Promise instance.
I think this could be done as a non-breaking change because it only affects cases that are currently compile errors.
The advantage of this would be that return type annotations on an async functions would work much the same as they do on normal functions and generator functions. It would resolve this issue and ones like it that are otherwise sure to come up from surprised users.
Would it be possible to alter the current implementation so that it works as the OP expected? That is, if an async function has its return type annotated with 'just a type', not a class (e.g. the PromiseLike interface), then that should not be an error as long as the annotation is type-compatible with Promise, and such a function would return a standard ES6 Promise.
for target ES3/ES5 i think this is more confusing that just using the type annotation. we should make the error message more expressive. for target ES6 and above, i do not see why not.
I tend to agree with @yortus that the current behavior is confusing since type annotation have an impact on the generated JS code. As you can see with #5998 the code generator doesn't handle this well either.
@dbaeumer, #5998 is a different issue. the import was not marked correctly, and was elided at emit time. this issue should be fixed in nightlies.
@dbaeumer, @mhegazy: Even if we changed the behavior for async functions to only use a "Promise" value that is in scope, we would still need to watch out for import elision. Consider:
import { Promise } from "promise";
export async function f() {}
We would still need to ensure Promise is not elided even though its only used in type position.
@yortus: If we were to drop the type-directed emit for the return-type annotation, there could be possible confusion for end users. Consider:
export class MyPromise<T> extends Promise<T> { }
export async function f(): MyPromise<T> { }
The above would still be legal, as we would check assignability of MyPromise<T> to Promise<T> as if MyPromise<T> were an interface, however:
console.log(f() instanceof MyPromise); // "false"
In C#, an async method _must_ return Task, Task<T> or void. If we dropped the ability to support custom Promise types or Promise subclasses from an async function, then our only option would be to ensure that the return type _must_ be Promise<T> to avoid possible confusion due to our structural type system. In that case, returning PromiseLike<T> would still not be legal.
If we wanted to be very strict about the return type, then we wouldn't even allow local redeclaration of Promise, and would only allow the use of a global Promise. I still contend that this would make it harder for developers to use async functions in a down-level host once we support down-level emit for generators.
for target ES3/ES5 i think this is more confusing that just using the type annotation. we should make the error message more expressive.
@mhegazy yes right, for ES3/ES5 it would be better to error since there is no builtin 'Promise'.
for target ES6 and above, i do not see why not.
hardly a ringing endorsement :) But I really hope this can be done for consistency's sake.
I tend to agree with @yortus that the current behavior is confusing since type annotation have an impact on the generated JS code.
@dbaeumer its not the confusion so much that bothers me, it's that it doesn't seem to reconcile with TypeScript's own design goals. I'm astounted that there hasn't been any direct response to this so far. The goals are clear - types should not change emitted code, and the type system should be fully erasable. The current implementation disregards both of these goals in its treatment of return type annotations. It would be nice at the very least for someone to acknowledge this. Maybe they are just general guidelines and not strict rules. That's fine - please just someone say something to the community about this.
We would still need to ensure
Promiseis not elided even though its only used in type position.
@rbuckton good point, this also applies to custom promises and promise polyfills in the current implementation - just filed #6003 for this.
@rbuckton you say users would be confused by this:
export class MyPromise<T> extends Promise<T> { }
export async function f<T>(): MyPromise<T> { }
console.log(f() instanceof MyPromise); // "false"
OK, this could be confusing for users who think that return type annotations affect runtime behaviour - which given that TypeScript's type system is supposed to be completely erasable _should_ be nobody. But let's say it is confusing for argument's sake. Here is the same code slightly modified to use generator functions instead:
export class MyIterator<T> implements Iterator<T> {/***/}
export function* f<T>(): MyIterator<T> { }
console.log(f() instanceof MyIterator); // "false"
Shouldn't this be just as confusing by the same argument? If this is _not_ confusing, isn't that because users know that return type annotations do not actually modify the generated code?
If we dropped the ability to support custom Promise types or Promise subclasses from an async function
I don't think anybody is suggesting dropping this support.
Anyway what you think of the suggestion here: https://github.com/Microsoft/TypeScript/issues/5911#issuecomment-162866988? This would give:
export class MyPromise<T> extends Promise<T> { }
export async function f<T>(): MyPromise<T> { }
console.log(f() instanceof MyPromise); // "true"
...because MyPromise is a class (ie exists in both type and value spaces) - so that would be handled exactly as it is currently. Then this example:
export async function f<T>(): PromiseLike<T> { }
console.log(f() instanceof Promise); // "true"
...would compile and work - because PromiseLike<T> is only a type (ie exists in type but not value space), so this would be a normal async function (ie not using custom promise) and the return type annotation would just need to be compatible with Promise.
If we dropped the ability to support custom Promise types [...], our only option would be to ensure that the return type must be
Promise<T>to avoid possible confusion due to our structural type system. In that case, returningPromiseLike<T>would still not be legal.
Here is this logic, as I understand it, expressed as a function:
function f(): PromiseLike<number> {
return Promise.resolve(42);
}
It compiles just fine. The return value _is_ a Promise<T> and the return type PromiseLike<T> _is_ legal. So there shouldn't be any problem annotating PromiseLike<T> on an async function that returns a Promise<T>.
In other words, if we _know_ the async function returns a Promise<T> (which was the premise of your comment) then we know PromiseLike<T> is a valid annotation because we know that Promise<T> is always assignable to PromiseLike<T>. Why would this not be legal?
@mhegazy, @rbuckton What do you think of the following minor change to async function parse/emit to address this issue? It's a non-breaking change and it makes type annotations on async functions consistent with how they behave for ordinary and generator functions.
async function foo(): P {/***/} // or if no annotation, treat P as Promise<any>
let isPromiseType = P is assignable to PromiseLike<any>
let isPromiseConstructor = typeof P is assignable to PromiseConstructorLike
if (isPromiseType) {
if (isPromiseConstructor) {
// use P as custom promise type in emit
emit async function with `P` as third argument to `__awaiter`
}
// <========== PROPOSED ADDITIONAL LOGIC ==========>
else if (target >= ES6) {
// P is not a constructable promise, but it is a promise-like type
// use builtin Promise class in emit
emit async function with `Promise` as third argument to `__awaiter`
}
// </========== PROPOSED ADDITIONAL LOGIC ==========>
else {
generate compiler error `"Type 'P' is not a valid async function return type"`
}
}
else {
generate compiler error `"Type 'P' is not a valid async function return type"`
}
class CustomPromise extends Promise<any> {}
class FooBar {}
interface Thenable {
then(onfulfilled?: (value) => any, onrejected?: (reason) => any): Thenable;
}
async function f1() { } // OK, returns a builtin Promise
async function f2(): CustomPromise {} // OK, returns a CustomPromise
async function f3(): FooBar {} // ERROR: 'FooBar' is not a valid async function return type
async function f4(): Thenable {} // OK, return type is Thenable, returns a builtin Promise
async function f5(): any {} // OK, return type is any, returns a builtin Promise
async function f6(): number {} // ERROR: number is not a valid async function return type
Note f4 and f5 are currently errors but would be valid with this proposed change.
// P is not a constructable promise, but it is a promise-like type
@yortus, the proposal above is actually a type-directed emit. The emit logic, in this proposal, depends on whether the type checker managed to correctly resolve the type from the type annotation, and correctly identified that it is constratable.
This breaks the transpile scenarios, where the compiler has access only to one file at a time. For these scenarios the transformation done by the emitter is expected to be a pure syntactic transformation on a file-level with no involvement of the type system what so ever. .d.ts files are not even loaded, even lib.d.ts is not loaded.
You have referred to the current implementation as "type-directed" emit. That is not correct. The current implementation will always emit the same output regardless of the return type, whether it is defined or not, whether the type checker resolved it correctly or not, or whether it is a single file transformation or a whole program compilation. This is one of the axioms that we have strived to maintain and have rejected a lot of proposals that will contradict it.
Just to elaborate, type-directed emit means that the emitted code for a given input is based on the view of the type system. if the type system thinks of a name as a number, it will emit it differently from if it thought it was a string.
The async function transformation has nothing to do with the type system, it is purely syntactic. it is however, type-annotation specific emit. for that, i do not see the problem, and i do not see how that is significantly different from using the value "Promise" from the current scope.
This breaks the transpile scenarios, where the compiler has access only to one file at a time.
This finally might explain something about why we seem to be at such cross purposes. Can you please confirm if I have understood the implications of this. Suppose the compiler needs to emit code for async (): M => {} but doesn't know what type M is. The current implementation _always_ emits M, leaving the question of validity of M in that type position to the checker. OTOH what I have proposed above needs to emit M if M is a constructable promise type, but if M is just an interface it needs to emit Promise instead. But it doesn't have information about M's type so it can't know what to emit.
Is that what you mean?
This may clear up a few things. I've been looking from an 'in principle' perspective. I.e. in principle, PromiseLike<T> is a valid type annotation for any function returning a Promise<T>, regardless whether it's (): PromiseLike<any> => Promise.resolve(42) or async (): PromiseLike<any> => 42. I mean in principle. There exists a compiler implementation that can do this. It is consistent with type theory.
OTOH I think you are speaking from an implementation perspective. The way the TypeScript compiler is structured, and which parts have access to which information at which time, and what compiler options must be supported (eg single file parse/emit) all constrain the possibilities in practice.
Would that be a fair assessment? I hope so because it has been very frustrating (I'm sure for you too) trying to understand the objections to supporting syntax like async (): PromiseLike<any> => 42, and why function* (): any {} is valid but async function (): any {} is an error.
The current implementation will always emit the same output regardless of the return type
If you mean the return type _annotation_, what about #6007?
You have referred to the current implementation as "type-directed" emit. That is not correct.
I just adopted that phrase because that's how @rbuckton and @DanielRosenwasser describe it here and here. Given #6007, I think it needs some special name because no other features emit different code when you remove a type annotation AFAIK.
+1
I met this issue in my scenario. My tsconfig is set to es6, module AMD(typescript 1.7.5) and my code is like this:
'use strict';
import { TPromise } from 'base/TPromise';
export class MyClass {
async f1(): TPromise<number> {
await this.use();
return new TPromise<number>((c,e,p)=> {
c(2);
});
}
async use(): TPromise<string>{
return new TPromise<string>((c,e,p)=> {
c('abc');
});
}
}
And it will generate:
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promise, generator) {
return new Promise(function (resolve, reject) {
generator = generator.call(thisArg, _arguments);
function cast(value) { return value instanceof Promise && value.constructor === Promise ? value : new Promise(function (resolve) { resolve(value); }); }
function onfulfill(value) { try { step("next", value); } catch (e) { reject(e); } }
function onreject(value) { try { step("throw", value); } catch (e) { reject(e); } }
function step(verb, value) {
var result = generator[verb](value);
result.done ? resolve(result.value) : cast(result.value).then(onfulfill, onreject);
}
step("next", void 0);
});
};
define(["require", "exports", 'base/TPromise'], function (require, exports, TPromise_1) {
'use strict';
class MyClass {
f1() {
return __awaiter(this, void 0, TPromise, function* () {
yield this.use();
return new TPromise_1.TPromise((c, e, p) => {
c(2);
});
});
}
use() {
return __awaiter(this, void 0, TPromise, function* () {
return new TPromise_1.TPromise((c, e, p) => {
c('abc');
});
});
}
}
exports.MyClass = MyClass;
});
TPromise is just like PromiseLike, it is another promise implementation which I grabbed from vscode(winjs).
So the error message is:
TPromise is undefined
In my case, I think it should generate something like (focus on TPromise_1.TPromise part)
return __awaiter(this, void 0, TPromise_1.TPromise, function* ()
We unfortunately cannot loosen the restriction around the return type annotation of an async function, as there may be existing user code that relies on the return type annotation pointing to a type that also has a reachable constructor value of the same name.
As of #6631 we are further restricting the return type annotation of an async function in ES6 or higher to be only Promise<T>. The rationale for that decision can be found here: https://github.com/Microsoft/TypeScript/pull/6631#discussion_r51040975.
I am closing this issue based on our current plans to restrict the return type annotation to only Promise<T>. We may reinvestigate this issue in a later release.
I've read through a bunch of these issues, and I have to raise this flag again.
Just now I installed 2.2 dev and I'm getting this error. I understand the concerns about how specifying the actual promise that ends up getting used is a good choice. I also understand how the awaiter works.
I have my own Promise lib. And I still don't see the value in making this more restrictive.
The PromiseLike
Most helpful comment
I've read through a bunch of these issues, and I have to raise this flag again. interface is... Well something should be available that is more generic/broad IMO.
Just now I installed 2.2 dev and I'm getting this error. I understand the concerns about how specifying the actual promise that ends up getting used is a good choice. I also understand how the awaiter works.
I have my own Promise lib. And I still don't see the value in making this more restrictive.
The PromiseLike