Skip to content

async / await and PromiseLike #5911

Closed
Closed

Description

@dbaeumer
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.

Activity

  1. mhegazy commented on Dec 3, 2015

    @mhegazy
    Contributor

    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.

  2. Arnavion commented on Dec 4, 2015

    @Arnavion
    Contributor

    Also, since you're returning a promise and not the value, you don't need to mark it as async anyway.

  3. dbaeumer commented on Dec 4, 2015

    @dbaeumer
    MemberAuthor

    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.

  4. yortus commented on Dec 4, 2015

    @yortus
    Contributor

    Dirk Bäumer (@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.

  5. dbaeumer commented on Dec 4, 2015

    @dbaeumer
    MemberAuthor

    Arnav Singh (@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);
    }
  6. yortus commented on Dec 4, 2015

    @yortus
    Contributor

    Dirk Bäumer (@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.

  7. yortus commented on Dec 4, 2015

    @yortus
    Contributor

    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.

  8. dbaeumer commented on Dec 4, 2015

    @dbaeumer
    MemberAuthor

    Troy Gerwien (@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.

  9. yortus commented on Dec 4, 2015

    @yortus
    Contributor

    Dirk Bäumer (@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);
    }
  10. yortus commented on Dec 4, 2015

    @yortus
    Contributor

    Dirk Bäumer (@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.

  11. dbaeumer commented on Dec 4, 2015

    @dbaeumer
    MemberAuthor

    Troy Gerwien (@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)

  12. jrieken commented on Dec 4, 2015

    @jrieken
    Contributor

    Dirk Bäumer (@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);
    
  13. yortus commented on Dec 4, 2015

    @yortus
    Contributor

    Dirk Bäumer (@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.

  14. dbaeumer commented on Dec 4, 2015

    @dbaeumer
    MemberAuthor

    Troy Gerwien (@yortus) what I meant with error prone is that people might forget it (especially if they come from a C# background).

  15. 40 remaining items

  16. rbuckton commented on Dec 8, 2015

    @rbuckton
    Contributor

    Dirk Bäumer (@dbaeumer), Mohamed Hegazy (@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.

  17. rbuckton commented on Dec 8, 2015

    @rbuckton
    Contributor

    Troy Gerwien (@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.

  18. yortus commented on Dec 9, 2015

    @yortus
    Contributor

    for target ES3/ES5 i think this is more confusing that just using the type annotation. we should make the error message more expressive.

    Mohamed Hegazy (@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.

  19. yortus commented on Dec 9, 2015

    @yortus
    Contributor

    I tend to agree with Troy Gerwien (@yortus) that the current behavior is confusing since type annotation have an impact on the generated JS code.

    Dirk Bäumer (@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.

  20. yortus commented on Dec 9, 2015

    @yortus
    Contributor

    We would still need to ensure Promise is not elided even though its only used in type position.

    Ron Buckton (@rbuckton) good point, this also applies to custom promises and promise polyfills in the current implementation - just filed #6003 for this.

  21. yortus commented on Dec 9, 2015

    @yortus
    Contributor

    Ron Buckton (@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: #5911 (comment)? 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, returning PromiseLike<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?

  22. yortus commented on Dec 10, 2015

    @yortus
    Contributor

    Mohamed Hegazy (@mhegazy), Ron Buckton (@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.

    Given this sample code
    async function foo(): P {/***/} // or if no annotation, treat P as Promise<any>
    Current compiler behaviour pseudocode, with proposed additional logic
    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"`
    }
    
    Results:
    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.

  23. mhegazy commented on Dec 10, 2015

    @mhegazy
    Contributor

    // P is not a constructable promise, but it is a promise-like type

    Troy Gerwien (@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.

  24. yortus commented on Dec 10, 2015

    @yortus
    Contributor

    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 Ron Buckton (@rbuckton) and Daniel Rosenwasser (@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.

  25. TheLevenCreations commented on Jan 23, 2016

    @TheLevenCreations

    +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* () 
  26. rbuckton commented on Jan 27, 2016

    @rbuckton
    Contributor

    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: #6631 (comment).

  27. rbuckton commented on Jan 27, 2016

    @rbuckton
    Contributor

    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.

  28. electricessence commented on Nov 19, 2016

    @electricessence

    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 interface is... Well something should be available that is more generic/broad IMO.

  29. locked and limited conversation to collaborators on Jun 19, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

BugA bug in TypeScript

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions