Typescript: Support async function type

Created on 13 Feb 2017  路  17Comments  路  Source: microsoft/TypeScript

TypeScript Version: 2.1.1 / nightly (2.2.0-dev.201xxxxx)

Can we support async function type? That would make a lot of async code much cleaner.

For example:

Code

type asyncFunctionResolvingToString = async () => string;

Expected behavior:
Because TS currently implements ES2016 async/await proposal, under the hood it'd be equivalent to:

type asyncFunctionResolvingToString = () => Promise<string>;

Note: using Promise is just an implementation detail and could change in the future. What would stay the same is the fact, that the method returns its values async and can be "awaited".


Actual behavior:
TS error:

TS: cannot find name async

Question

Most helpful comment

@verticalpalette I've personally found it's almost never actually useful to draw a distinction between the two when you're not actually needing to parse the function. And when you actually do need to draw the distinction without parsing, it's almost always for pretty-printing, etc., not actually matching the function or anything.

Oh, and also, your defer use case should really be a Babel transform or something similar - proper semantics would place it in a try { ... } finally { ... } for async functions (where your close code goes in the finally, and a wrapped variant for other types:

// Original:
const myAsyncFunc = deferring(async defer => {
  const resource = openResource();
  defer(() => resource.close());
  // readAsync throws an error because the resource is closed immediately
  const contents = await resource.readAsync();
  return performComputation(contents);
});

const myPromiseFunc = deferring(defer => {
  const resource = openResource();
  defer(() => resource.close());
  // much more obvious that readAsync will throw an error
  return resource.readAsync().then(performComputation);
});

// Transformed:
const myAsyncFunc = async () => {
  const resource = openResource();
  try {
    // readAsync throws an error because the resource is closed immediately
    const contents = await resource.readAsync();
    return performComputation(contents);
  } finally {
    resource.close()
  }
};

const myPromiseFunc = () => {
  const resource = openResource();
  const close$ = () => resource.close();
  let returning$ = false;
  try {
    const p$ = run();
    if (returning$ = p$ == null || typeof p$.then !== "function") return p$;
    return new Promise((resolve, reject) => {
      p$.then(
        x => { try { close(); resolve(x) } catch (e) { reject(e) } },
        e => { try { close(); reject(e) } catch (e) { reject(e) } }
      );
    });
  } finally {
    if (returning$) close$();
  }
};

And honestly, it's rare for try/finally to not really be useful in these kinds of scenarios. Yes, it's explicit management (and no Java try-with-resources or Python with - I've proposed similar in es-discuss before, to meet mostly crickets), but it works well enough.

All 17 comments

I think that would be misleading because it would suggest that returning a Promise and being async are the same thing, but a function that returns a Promise may or may not actually be async (it could return a Promise directly). From the caller's perspective async is an implementation detail.

@jesseschalken TS implements ES2016 async/away proposal which explicitly states, that the return value is a Promise. So the implication of the return value is valid, unless I'm missing something ...

Could you provide an example where this wouldn't be true ?

I'm not saying async doesn't imply the return value is a Promise. I'm saying the return value being a Promise doesn't imply that the function is async.

a function that returns a Promise may or may not actually be async (it could return a Promise directly)

@jesseschalken an async function returns its Promise directly too (i.e. the function returns synchronously although the returned Promise may resolve later). In this respect there is no observable difference from any other Promise-returning function. They all synchronously return a promise. Can you elaborate on the difference you are talking about?

@yortus When I say "is async" I mean literally defined using the async keyword. async () => 1 is async i.e. uses the ECMAScript feature called "async/await", and returns a Promise. () => Promise.resolve(1) is not async, but still returns a Promise. I'm just talking about how the function happens to be written. The caller can't tell them apart.

If you add the async (..) => .. syntax as an alias for (..) => Promise<..>, then it suggests that async and Promise are the same concept. They're not. async is _one way_ of _implementing_ a function that returns a Promise.

@jesseschalken OK I get you now. But still, there's no observable distinction between the two ways of writing the same thing. Isn't it a bit like saying type P = { foo(): string } is different from type Q = { foo: () => string }? The distinctions are more about convenience and history.

I believe that from the caller perspective (and from one who implements an interface) it makes sense to know which methods are supposed to be async and can be awaited.

It aligns nicely with checking the resolved value of promises.

From the caller's perspective async is an implementation detail.

I believe the opposite is true. "Async" describes a pattern, while Promise<type> describes a behaviour (return value). Consumer of this method/interface would see, that this particular method is async, so that they can use .then() syntax or await the result as per the pattern. The fact that internally it's an instance of a Promise is the implementation detail here.

Imagine, hypothetically, at some point in the future Promise gets replaced with Future. For consumers that rely on async/away syntax, nothing would change because async function stay async, even though the implementation method has changed.

I've updated this issue's description to point out, that the scope is having a way to "specify an async method" as opposed to making it equivalent to Promise return value.

Note: using Promise is just an implementation detail and could change in the future. What would stay the same is the fact, that the method returns its values async and can be "awaited".

@Thinkscape returning Promise is not just an implementation detail, it is a visible part of the published interface of async functions. It will not change in the future in a breaking way because the web eschews backward-incompatible changes.

Also, await operates on Promise values which can come from any source. It makes no distinction whether they originate from an async function or anywhere else. So this is not a justification for the syntax proposed.

The only justification I can see for the syntax you propose is convenience. It doesn't introduce any functional distinctions IMO. It also brings back up the same issue of potentially misleading return type annotation that was discussed in #7284.

I believe the opposite is true. "Async" describes a pattern, while Promise describes a behaviour (return value).

A caller _cannot_ tell the difference between async () => 1 and () => Promise.resolve(1). One uses the async syntax, one does not. They both return a Promise. The Promise is the interface. Whether the function uses async or not is an implementation detail because _it makes no difference to the caller_.

Consumer of this method/interface would see, that this particular method is async, so that they can use .then() syntax or await the result as per the pattern.

You can use .then() and await on _any Promise_. Whether the Promise happened to be produced using async syntax is irrelevant. Only the fact that the value is a Promise is relevant to the caller.

Imagine, hypothetically, at some point in the future Promise gets replaced with Future. For consumers that rely on async/away syntax, nothing would change because async function stay async, even though the implementation method has changed.

A Promise has a specific set of methods and specific semantics. Promise _is_ the interface. You even have to write Promise as the return type when defining an async function. Replacing Promise with a class with a different set of methods with different semantics (eg Future) is necessarily a breaking change.

IMHO The discussion is on a sidetrack... whether or not there is a difference to a client, it would be nice if an async function could be declared as having the type of an async function. In fact this is more important if async functions are different than functions returning promises.

BTW there is discernable difference between a function an an async function, at least in node:

> const f = async () => {} 
> f.constructor.name
'AsyncFunction'

They don't make a difference to the type system:

function foo() { };

async function bar() { };

(async () => {
    const baz = await foo();
    const qat = await bar();
})();

Be they async or not, externally, from a type perspective, they are both _awaitable_ and the type resolution is clear. Because they don't make a difference to how the functions are consumed from a type or code perspective, they have no place in TypeScript's type system. async obviously has meaning to the run-time handles, allocates and invokes those functions, but TypeScript doesn't have to worry about that at a type level (only an down-emit level).

Hmm... if I have a javascript function:

const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor
...
async function takesAsyncOrObject(asyncOrObject) {
  if (asyncOrObject instanceof AsyncFunction) {
      asyncOrObject = await asyncOrObject()
  }
  return asyncOrObject
}

How should I notate it in typescript? It would seem best would be:

asyncOrObject : object | async () => object

Then the return type of the overall function can be object.

You can do more to a function besides calling it. Async functions and functions should be different types because they have different constructors. They can inherit from a common base type of course....

@shaunc Couple tips:

  1. Type it as this:

    async function takesAsyncOrObject<T>(asyncOrObject: T | (() => T | Promise<T>)) { ... }
    
  2. Check for typeof asyncOrObject === "function" instead.

They have different constructors, but they are both callable, and TS only cares about that much. In particular, it doesn't even type generator functions as anything beyond (...args) => Iterator<T>. It's extremely brittle to check for anything more specific than a callable type for anything you just plan to call.

TypeScript is a structurally typed language, not nominally, so leverage that to your advantage. Also, it's bad practice in general to check types much narrower than what you actually need, this being no exception.

Because they don't make a difference to how the functions are consumed from a type or code perspective, they have no place in TypeScript's type system.

It's not obvious to me that async functions _shouldn't_ be distinguished inside TypeScript's type system.

For example, I was playing around with adding Go's defer keyword as a function that wraps another function. For example, one could use it like:

const myFunc = deferring(defer => {
  const resource = openResource();
  defer(() => resource.close());
  const contents = resource.readSync();
  return performComputation(contents);
});

But using deferring naively with an async function would produce a surprising result:

const myAsyncFunc = deferring(async defer => {
  const resource = openResource();
  defer(() => resource.close());
  // the resource is never closed because the `defer` is called after the function returns
  const contents = await resource.readAsync();
  return performComputation(contents);
});

Even though using deferring with a normal function that returns a Promise is not surprising:

const myPromiseFunc = deferring(defer => {
  return new Promise((resolve, reject) => {
    const resource = openResource();
    defer(() => resource.close());
    // much more obvious that there's an error
    return resource.readAsync();
  }).then(performComputation);
});

Ideally deferring could accept functions that return Promises, but wouldn't accept async functions as part of its type signature, to prevent this kind of surprise.


Obviously this isolated use-case isn't enough to warrant a change to the language, but I think it's suggestive that preventing programs from knowing whether something is an async function vs. a normal function that returns a Promise will probably prevent cool and useful stuff (probably lots of which we haven't imagined yet). I have no idea whether it's worth the cost, though.

@verticalpalette I've personally found it's almost never actually useful to draw a distinction between the two when you're not actually needing to parse the function. And when you actually do need to draw the distinction without parsing, it's almost always for pretty-printing, etc., not actually matching the function or anything.

Oh, and also, your defer use case should really be a Babel transform or something similar - proper semantics would place it in a try { ... } finally { ... } for async functions (where your close code goes in the finally, and a wrapped variant for other types:

// Original:
const myAsyncFunc = deferring(async defer => {
  const resource = openResource();
  defer(() => resource.close());
  // readAsync throws an error because the resource is closed immediately
  const contents = await resource.readAsync();
  return performComputation(contents);
});

const myPromiseFunc = deferring(defer => {
  const resource = openResource();
  defer(() => resource.close());
  // much more obvious that readAsync will throw an error
  return resource.readAsync().then(performComputation);
});

// Transformed:
const myAsyncFunc = async () => {
  const resource = openResource();
  try {
    // readAsync throws an error because the resource is closed immediately
    const contents = await resource.readAsync();
    return performComputation(contents);
  } finally {
    resource.close()
  }
};

const myPromiseFunc = () => {
  const resource = openResource();
  const close$ = () => resource.close();
  let returning$ = false;
  try {
    const p$ = run();
    if (returning$ = p$ == null || typeof p$.then !== "function") return p$;
    return new Promise((resolve, reject) => {
      p$.then(
        x => { try { close(); resolve(x) } catch (e) { reject(e) } },
        e => { try { close(); reject(e) } catch (e) { reject(e) } }
      );
    });
  } finally {
    if (returning$) close$();
  }
};

And honestly, it's rare for try/finally to not really be useful in these kinds of scenarios. Yes, it's explicit management (and no Java try-with-resources or Python with - I've proposed similar in es-discuss before, to meet mostly crickets), but it works well enough.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

CyrusNajmabadi picture CyrusNajmabadi  路  3Comments

siddjain picture siddjain  路  3Comments

Zlatkovsky picture Zlatkovsky  路  3Comments

kyasbal-1994 picture kyasbal-1994  路  3Comments

Antony-Jones picture Antony-Jones  路  3Comments