Reputation: 61
I am writing a module aims at preparing a query before calling it to database. The code in vanilla javascript worked pretty well, but when I tried to write it in Typescript, I got the error: Type of 'await' operand must either be a valid promise or must not contain a callable 'then' member
My code in Javascript:
class QueryBuilder {
constructor(query) {
this.query = query;
}
sort(keyOrList, direction) {
this.query = this.query.sort(keyOrList);
return this;
}
skip(value) {
this.query = this.query.skip(value);
return this;
}
limit(value) {
this.query = this.query.limit(value);
return this;
}
then(cb) {
cb(this.query.toArray());
}
}
Code in Typescript:
class QueryBuilder {
public query: Cursor;
constructor(query: Cursor) {
this.query = query;
}
public sort(keyOrList: string | object[] | object, direction: any) {
this.query = this.query.sort(keyOrList);
return this;
}
public skip(value: number) {
this.query = this.query.skip(value);
return this;
}
public limit(value: number) {
this.query = this.query.limit(value);
return this;
}
public then(cb: Function) {
cb(this.query.toArray());
}
}
How I called these methods:
const query = await new QueryBuilder(Model.find())
.limit(5)
.skip(5)
Hope someone can help me with this. Thanks in advance.
*Updated:
I extended QueryBuilder class from the buitin Promise, then overrided then
method by QueryBuilder.prototype.then
.The code is now executable but I didn't truly understand the super(executor)
in the constructor. It's required an executor(resolve: (value?: T | PromiseLike<T> | undefined) => void, reject: (reason?: any) => void): void
so I just simply created a dumb executor. How does it affect to the code?
class QueryBuilder<T> extends Promise<T> {
public query: Cursor;
constructor(query: Cursor) {
super((resolve: any, reject: any) => {
resolve("ok");
});
this.query = query;
}
public sort(keyOrList: string | object[] | object, direction?: any) {
this.query = this.query.sort(keyOrList);
return this;
}
public skip(value: number) {
this.query = this.query.skip(value);
return this;
}
public limit(value: number) {
this.query = this.query.limit(value);
return this;
}
}
QueryBuilder.prototype.then = function (resolve: any, reject: any) {
return resolve(this.query.toArray());
};
Upvotes: 6
Views: 2303
Reputation: 9731
The TypeScript algorithm for evaluating the type of the operand for await
goes something like this (it's a very simplified explanation)[reference]:
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)
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());
}
}
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.
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>
.
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.
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:
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.
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 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?
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:
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:
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();
Upvotes: 4