hegel icon indicating copy to clipboard operation
hegel copied to clipboard

Unsound `await` handling

Open vkurchatkin opened this issue 5 years ago • 5 comments

async function foo<T>(val: T) {
  const v = await val;
  return v;
}

async function test() {
  const promise = Promise.resolve(1);
  const val = await foo(promise);
  const a = val.then(); // Runtime error
}

test();

I assume that await and similar promise methods have signature like this:

await<T>(Promise<T> | T) => T;

This is incorrect. If type of T is not known to be a promise (i.e. unknown or generic T), it is assumed NOT to be a promise, while at runtime it could be a promise.

vkurchatkin avatar May 21 '20 22:05 vkurchatkin

Hmmm. I didn't catch a runtime error. Could you please explain the problem in detail?

JSMonk avatar May 21 '20 22:05 JSMonk

Sorry, typo. Fixed the example

vkurchatkin avatar May 21 '20 22:05 vkurchatkin

Fixed in 0.0.45

JSMonk avatar Jun 10 '20 20:06 JSMonk

Not really fixed though. A tiny modification and you get the same thing:

async function foo<T>(val: T): Promise<T> {
  const v = await val;
  return v;
}

async function test() {
  const promise: Promise<1> | string = Promise.resolve(1);
  const val = await foo(promise);

  if (typeof val !== 'string') {
    const a = val.then(); // Runtime error
  }
}

test();

vkurchatkin avatar Jun 11 '20 14:06 vkurchatkin

Let's say there is a generic type for that called $Await<T> which is supposed to evaluate to the type of expression await x where x has type T.

T can be one of the following

Thenable<T>

interface Thenable<T> {
  then<T>(fn: (val: T) => unknown): unknown;
}

For thenables, $Await<Thenable<T>> equals $Await<T>.

False-thenable

False-thenable is an object, for which obj.then evaluates to a function, but it's not actually a thenable. Applying $Await to a false-thenable makes no sense, since there is no way to evaluate it.

Non-thenable

For everything else $Await<T> equals T;

So here is a possible way to deal with that. await x (or Promise.resolve(x) or other) evaluates to type $Await<T>. Type $Await<T> can be further refined if we have additional information about T. If T is known to be a thenable, we evaluate $Await<Thenable<T>> to $Await<T> recursively. If T is known to be a non-thenable, $Await<T> evaluate to T. Otherwise $Await<T> should stay opaque, since there is no way to tell what it is.

Additionally, it should be illegal to await on values, that could possibly be false-thenables. Here is an example:

async function test(x: unknown) {
  await x; // This should not be allowed
}


const obj = {
  then(str: string) {
    return str.toLowerCase(); // runtime error
  }
}

test(obj);

vkurchatkin avatar Jun 12 '20 15:06 vkurchatkin