Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support async function type #14027

Closed
Thinkscape opened this issue Feb 13, 2017 · 17 comments
Closed

Support async function type #14027

Thinkscape opened this issue Feb 13, 2017 · 17 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@Thinkscape
Copy link

Thinkscape commented Feb 13, 2017

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

@jesseschalken
Copy link
Contributor

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.

@Thinkscape
Copy link
Author

@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 ?

@jesseschalken
Copy link
Contributor

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.

@yortus
Copy link
Contributor

yortus commented Feb 13, 2017

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?

@jesseschalken
Copy link
Contributor

jesseschalken commented Feb 13, 2017

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

@yortus
Copy link
Contributor

yortus commented Feb 13, 2017

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

@Thinkscape
Copy link
Author

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.

@Thinkscape
Copy link
Author

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.

@Thinkscape
Copy link
Author

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.

@yortus
Copy link
Contributor

yortus commented Feb 14, 2017

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.

@jesseschalken
Copy link
Contributor

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.

@mhegazy mhegazy added the Question An issue which isn't directly actionable in code label Feb 14, 2017
@mhegazy mhegazy closed this as completed Apr 21, 2017
@shaunc
Copy link

shaunc commented Jun 27, 2017

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'

@kitsonk
Copy link
Contributor

kitsonk commented Jun 27, 2017

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

@shaunc
Copy link

shaunc commented Jun 27, 2017

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

@dead-claudia
Copy link

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

@verticalpalette
Copy link

verticalpalette commented Jan 3, 2018

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.

@dead-claudia
Copy link

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

@microsoft microsoft locked and limited conversation to collaborators Jul 3, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

8 participants