Unsound `await` handling
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.
Hmmm. I didn't catch a runtime error. Could you please explain the problem in detail?
Sorry, typo. Fixed the example
Fixed in 0.0.45
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();
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);