Problem
What is TypeScript doing?
The TypeScript algorithm for evaluating the type of the operand for await
goes something like this (it's a very simplified explanation)[reference]:
- Is the type a promise? If yes go to step 1 with the type that is promised. If no go to step 2.
- Is the type a thenable? If no return the type. If yes throw a type error saying "Type of 'await' operand must either be a valid promise or must not contain a callable 'then' member (ts1320)".
What is TypeScript doing in your example?
Now knowing this, we can see what TypeScript is doing when checking your code.
The operand to await
is:
new QueryBuilder(Model.find())
.limit(5)
.skip(5)
- The call to
skip
doesn't return a promise. We go to step 2 (Note: neither does the call to limit
or the instantiation of QueryBuilder
).
skip
returns the instance of QueryBuilder
which has a callable then
member. This results in the type error: "Type of 'await' operand must either be a valid promise or must not contain a callable 'then' member (ts1320)".
Your class definition with a callable 'then' member:
class QueryBuilder {
public query: Cursor;
constructor(query: Cursor) {
this.query = query;
}
...
public then(cb: Function) {
cb(this.query.toArray());
}
}
Why does TypeScript error?
Now we understand how TypeScript threw the type error. But why does it throw this error? JavaScript lets you await
on anything.
[rv] = await expression;
expression: A Promise or any value to wait for.
rv: Returns the fulfilled value of the promise, or the value itself if it's not a Promise.
– MDN documentation on await
Why does TypeScript say "Type of 'await' operand [if it's not a valid promise] must not contain a callable 'then' member"? Why does it not let you await
on a thenable? MDN even gives an example where you await
on a thenable.
async function f2() {
const thenable = {
then: function(resolve, _reject) {
resolve('resolved!')
}
};
console.log(await thenable); // resolved!
}
f2();
– MDN example await
ing a thenable
TypeScript's source code is helpfully commented. It reads:
The type was not a promise, so it could not be unwrapped any further.
As long as the type does not have a callable "then" property, it is
safe to return the type; otherwise, an error is reported and we return
undefined.
An example of a non-promise "thenable" might be:
await { then(): void {} }
The "thenable" does not match the minimal definition for a promise.
When a Promise/A+-compatible or ES6 promise tries to adopt this value,
the promise will never settle. We treat this as an error to help flag
an early indicator of a runtime problem. If the user wants to return
this value from an async function, they would need to wrap it in some
other value. If they want it to be treated as a promise, they can cast
to <any>
.
– Reference
From reading this, my understanding is TypeScript does not await
on non-promise thenables because it cannot guarantee the implementation matches the minimum spec as defined by Promises/A+ thus assumes it is an error.
Comments on your solution
In the solution that you've tried and added to the context of your question you've defined the QueryBuilder
class to extend off of the native promise and then you've overridden the then
member. While this seems to have the effect you want there are some problems:
Your class instantiation has unreasonable behaviour
As a result of extending a class you need to call its parent constructor before you can reference the this
context. The type for parent class' constructor is:
(resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void
And as you've found you need to pass in something that satisfies that contract for no other reason than to get it to work. Also after instantiation your class returns a promise resolved with an arbitrary value. Code that doesn't matter and is unreasonable is a source of confusion, unexpected behaviour and is a potential for bugs.
You've broken the type contract defined by the promise interface
The interface defines other methods such as catch
. A consumer of the class may attempt to use the catch
method and while the contract allows them to, the behaviour is not as expected. This leads on to the next point.
You're not using TypeScript to its advantage
You mentioned in your question:
The code in vanilla JavaScript worked pretty well, but when I tried to write it in TypeScript, I got the error
Types exist for a reason, one being they reduce the possibility of bugs by enforcing the contract between interfaces. If you try to work around them it leads to confusion, unexpected behaviour and an increased risk of bugs. Why use TypeScript in the first place?
Solution
Now that we understand what the error it is and why it is happening we can figure a solution. The solution will require the implementation of the then
member to meet the minimum spec in Promises/A+ as we have determined this to be the cause of the error. We only care about the spec for the then
interface, as opposed to its implementation details:
- 2.2.1 Both
onFulfilled
and onRejected
are optional arguments
- 2.2.7
then
must return a promise
The TypeScript definition for then
is also useful to reference (note: I've made some changes for readability):
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of which ever callback is executed.
*/
then<
TResult1 = T,
TResult2 = never
>(
onfulfilled?:
| ((value: T) => TResult1 | PromiseLike<TResult1>)
| undefined
| null,
onrejected?:
| ((reason: any) => TResult2 | PromiseLike<TResult2>)
| undefined
| null
): Promise<TResult1 | TResult2>;
The implementation itself will likely follow this algorithm:
- Execute your custom logic
- If step 1 was successful resolve with the result of your custom logic, otherwise reject with the error
Here is a demo of an example implementation that should get you started on your own implementation.
class CustomThenable {
async foo() {
return await 'something';
}
async then(
onFulfilled?: ((value: string) => any | PromiseLike<string>) | undefined | null,
): Promise<string | never> {
const foo = await this.foo();
if (onFulfilled) { return await onFulfilled(foo) }
return foo;
}
}
async function main() {
const foo = await new CustomThenable();
console.log(foo);
const bar = await new CustomThenable().then((arg) => console.log(arg));
console.log(bar);
}
main();
await
in thequery
initialization statement. QueryBuilder class initialization operation doesnt return anypromise
, hence the error. – Holschain
the operations even after removing theawait
from the query instantiation statement. – Holsthis.query.toArray()
presumably returns a Promise and hence, you have toawait
its result. The chaining itself is also not the problem. The point of OP's code is to actually execute the async query and await its result when used withawait
. – Holzer.then
method if it doesn't conform to the Promises/A+ spec (it doesn't). You don't want to set off another flame war. – Input.then
function in such a way that it is not Promisea/A+ compliant butasync
is still able to handle it, but it's a horrible practice. If someone actually tried to use your.then
method directly, they would wind up with a Zalgo on their hands. They'd also wind up with an error if they understandably tried to chain off of it. – Input