I also asks this on Stackoverflow, but I think this is a bug:
I'm trying to cancel my async method call in Typescript.
To do this, I have created a new Promise type, which inherits from Promise.
My first version was without the Object.setPrototypeOf(..), but I took it for here
TypeScript Version: 2.2.0, targeting ES5
Code
class CancelablePromise<T> extends Promise<T>{
public cancelMethod: () => void;
constructor(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void) {
super(executor);
// Set the prototype explicitly.
// See: https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
Object.setPrototypeOf(this, CancelablePromise.prototype);
}
//cancel the operation
public cancel() {
if (this.cancelMethod) {
this.cancelMethod();
}
}
}
class Test{
async postFileAjax<T>(file: File): CancelablePromise <T> {
var promiseFunc = (resolve) => { resolve() };
var promise = new CancelablePromise<T>(promiseFunc);
promise.cancelMethod = () => { console.log("cancel!") };
return promise;
}
}
var test = new Test();
test.postFileAjax(null);
Expected behavior:
No runtime error, or compile error if not allowed.
And of course working promise if no error.
Actual behavior:
Got a run-time error
Error:
(unknown) Uncaught TypeError: undefined is not a promise
at CancelablePromise.Promise (<anonymous>)
at new CancelablePromise (<anonymous>:44:28)
at __awaiter (<anonymous>:7:12)
at Test.postFileAjax (<anonymous>:62:16)
at <anonymous>:75:6
at HTMLButtonElement.excuteButton.onclick (https://www.typescriptlang.org/play/playground.js:242)
Link to playground
The issue here is that ES6 classes (e.g. Promise) are not callable, the way ES5 built in classes (e.g. Array). The emitted code that the compiler produces uses super.call which throws in ES6 engine.
The fix here is to use Reflect.construct if it exists instead of call. Reflect.construct is not something we can polifill, but we can assume if built-in Promise is there, Refelect.construct is there too.
Here is a proposal for that from @rbuckton:
var __construct = (this && this.__construct) || (typeof Reflect !== "undefined" && Reflect.construct
? function (self, target, args) { return self !== null && Reflect.construct(target, args, self.constructor) || self; }
: function (self, target, args) { return self !== null && target.apply(self, args) || self; });
var PatchedPromise = (function (_super) {
__extends(PatchedPromise, _super);
function PatchedPromise(executor) {
var _this = this;
_this = __construct(this, _super, [executor]);
return _this;
}
return PatchedPromise;
}(Promise));
I have filed #15397 for this proposal.
FWIW, another workaround that seems to work is wrapping the promise in a class that's a "thenable". Here's one for a Deferred promise:
export default class Deferred<T> {
private res: (value?: T | PromiseLike<T>) => void
private rej: (reason?: any) => void
private readonly promise: Promise<T>
constructor() {
this.promise = new Promise((resolve, reject) => {
this.res = resolve
this.rej = reject
})
}
then(
onfulfilled?: (value: T) => T | PromiseLike<T>,
onrejected?: (reason: any) => PromiseLike<never>
): Promise<T> {
return this.promise.then(onfulfilled, onrejected)
}
catch(onRejected?: (reason: any) => PromiseLike<never>): Promise<T> {
return this.promise.catch(onRejected)
}
resolve(value?: T | PromiseLike<T>): void {
return this.res(value)
}
reject(reason?: any): void {
return this.rej(reason)
}
}
Seems to work with async and whatnot.
closing in favor of #15397
Most helpful comment
FWIW, another workaround that seems to work is wrapping the promise in a class that's a "thenable". Here's one for a Deferred promise:
Seems to work with
asyncand whatnot.