Skip to content

Suggestion: Implied Promise return type in async functions #7284

Closed

Description

@nevir

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>:

async function makeStuff():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.

async function makeStuff():Stuff {}

Seems like the least surprising way of declaring an async function

Activity

  1. yortus commented on Feb 29, 2016

    @yortus
    Contributor

    Would you suggest doing the same for generator functions for consistency:

    function* foo() { // inferred return type is `IterableIterator<number>`
        yield 1;      // change that to just `number` under this proposal?
        yield 2;
        yield 3;
    }

    Also, I think a case could be made for this change being equally confusing, even as you say when the Promise token appears nowhere in the source:

    function foo(): string {/***/}
    var s1: string = foo(); // OK
    
    async function bar(): string {/***/}
    var s2: string = bar(); // ERROR
  2. nevir commented on Feb 29, 2016

    @nevir
    Author

    Would you suggest doing the same for generator functions for consistency:

    I think that would make sense; though I haven't wrapped my head around generators enough to be confident.


    Also, I think a case could be made for this change being equally confusing, even as you say when the Promise token appears nowhere in the source:

    async function bar(): string {/***/}
    var s2: string = bar(); // ERROR

    That's a good error to have! Lets you know that you should be awaiting bar(). Ideally the error message would hint about await, as well.

  3. tinganho commented on Feb 29, 2016

    @tinganho
    Contributor

    I also think it is annoying to have to specify the Promise type, when it can be inferred from the async keyword already.

  4. ljqx commented on Feb 29, 2016

    @ljqx

    Then how could we specify that we want to use Bluebird's Promise, not the original one?

  5. nevir commented on Feb 29, 2016

    @nevir
    Author

    Then how could we specify that we want to use Bluebird's Promise, not the original one?

    As of #6631, you can't do that any more (except by providing your own __awaiter implementation)

  6. yortus commented on Feb 29, 2016

    @yortus
    Contributor

    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?

  7. tinganho commented on Feb 29, 2016

    @tinganho
    Contributor

    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.

  8. yortus commented on Feb 29, 2016

    @yortus
    Contributor

    I think a fuller example showing the pain point / motivating scenario would be useful. The OP gave this example:

    async function makeStuff():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?

  9. tinganho commented on Feb 29, 2016

    @tinganho
    Contributor

    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.

  10. yortus commented on Feb 29, 2016

    @yortus
    Contributor

    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:

    export class Foo {
        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:

    export class Foo {
        async makeStuff(): Thenable<Stuff> {
            // make some stuff...
            // returns an ES6 Promise of stuff, which is-a Thenable<Stuff>
        }
    }

    Another way would be like this:

    export class Foo {
        makeStuff(): Thenable<Stuff> {
            return new bluebird.Promise(...)
        }
    }

    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.

  11. tinganho commented on Feb 29, 2016

    @tinganho
    Contributor

    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?

    interface A {
        async method(): string;
    }
  12. yortus commented on Feb 29, 2016

    @yortus
    Contributor

    How is this:

    interface A {
        async method(): string;
    }

    different from this:

    interface A {
        method(): Promise<string>;
    }

    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.

  13. tinganho commented on Feb 29, 2016

    @tinganho
    Contributor

    Troy Gerwien (@yortus) my thought was that there is no difference between them.

  14. DanielRosenwasser commented on Feb 29, 2016

    @DanielRosenwasser
    Member
    interface A {
        async method(): string;
    }

    Actually, that's not a valid signature - async isn't allowed as part of a type (since all you need to know is that there's promise returned).

  15. tinganho commented on Feb 29, 2016

    @tinganho
    Contributor

    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?

  16. 4 remaining items

  17. Arnavion commented on Feb 29, 2016

    @Arnavion
    Contributor

    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.

  18. yortus commented on Mar 1, 2016

    @yortus
    Contributor

    Ian MacLeod (@nevir) do you have a motivating example with code?

  19. nevir commented on Mar 1, 2016

    @nevir
    Author

    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)

  20. jods4 commented on Mar 5, 2016

    @jods4

    ⚠️ 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.

  21. nevir commented on Mar 5, 2016

    @nevir
    Author

    it would be impossible to declare an async method as returning Promise | number.

    It is my understanding that an async function must return a Promise; it would be a compiler error not to.

    At least, that's how I'm interpreting section 1.1.8 of the async/await spec. All conditions of EvaluateBody return a Promise. This is also the current behavior of both TypeScript and Babel.


    I.e.

    async function foo() { return 123; }
    const result = foo(); // This is a Promise that resolves to 123.
  22. jods4 commented on Mar 5, 2016

    @jods4

    This is not accepted by the compiler today, but desirable

    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.

  23. nevir commented on Mar 6, 2016

    @nevir
    Author

    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.

    The async/await spec's repo would be a better place to bring it up.

  24. jods4 commented on Mar 6, 2016

    @jods4

    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.

  25. yortus commented on Mar 6, 2016

    @yortus
    Contributor

    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:

    import * as BluebirdPromise from 'bluebird';
    
    export interface Thenable<R> {
        then<U>(onFulfilled?: (value: R) => U | Thenable<U>, onRejected?: (error: any) => U | Thenable<U>): Thenable<U>;
        then<U>(onFulfilled?: (value: R) => U | Thenable<U>, onRejected?: (error: any) => void): Thenable<U>;
        catch<U>(onRejected?: (error: any) => U | Thenable<U>): Thenable<U>;
    }
    
    export function getFoo(): Thenable<Foo> {
        // returns a BluebirdPromise...
        return new BluebirdPromise((resolve, reject) => {...});
    }
    export function getBar(): Thenable<Bar> {
        // returns a BluebirdPromise...
        return getFoo().then(...).then(...);
    }

    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.

    The obvious approach would be:

    export async getBar(): Thenable<Bar> {
        let foo = await getFoo();
        ...
        return bar;
    }

    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.
  26. added
    Working as IntendedThe behavior described is the intended behavior; this is not a bug
    and removed
    SuggestionAn idea for TypeScript
    on May 9, 2016
  27. RyanCavanaugh commented on May 9, 2016

    @RyanCavanaugh
    Member

    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.

  28. 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

Assignees

No one assigned

    Labels

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions