Reputation: 10667
I want to extend native Javascript Promise class with ES6 syntax, and be able to call some asynchronous function inside the subclass constructor. Based on async function result the promise must be either rejected or resolved.
However, two strange things happen when then
function is called:
class MyPromise extends Promise {
constructor(name) {
super((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 1000)
})
this.name = name
}
}
new MyPromise('p1')
.then(result => {
console.log('resolved, result: ', result)
})
.catch(err => {
console.error('err: ', err)
})
Upvotes: 27
Views: 7720
Reputation: 6039
Summarizing the above into a short tl;dr for those who just want to get something working and move on, you just need to add this to your class body:
static get [Symbol.species]() {
return Promise;
}
A full TypeScript example:
class MyPromise extends Promise<string> {
constructor(name: string) {
super((resolve) => {
setTimeout(() => {
resolve(name)
}, 1000)
})
this.name = name;
}
static get [Symbol.species]() {
return Promise;
}
}
const p = new MyPromise('hi');
p.name === 'hi'
const x: string = await p;
x === 'hi'
That's all you need.
Forgoing Symbol.toStringTag
may make MyPromise
just look like a Promise
in some debugging contexts, and you won't be able to do MyPromise.race
or MyPromise.all
, but that's fine: you can use Promise.all
with a list of MyPromise
s.
Upvotes: 0
Reputation: 14759
You have to make it then
able by implementing the then
method.
Otherwise, that of the superclass, Promise
, will be called, and it’ll try to create another Promise
with your MyPromise
’ constructor, which is not compatible with the original Promise
constructor.
The thing is, it’s tricky to properly implement the then
method that works just like Promise
’s does. You’ll likely end up having an instance of Promise
as a member, not as a superclass.
Upvotes: 0
Reputation: 19301
The reasoning is simple but not necessarily self evident.
.then()
returns a promisethen
is called on a subclass of Promise, the returned promise is an instance of the subclass, not Promise itself.then
returned promise is constructed by calling the subclass constructor, and passing it an internal executor function that records the value of resolve
and reject
arguments passed to it for later use.then
asynchronously when monitoring execution of onfulfilled
or onrejected
handlers (later) to see if they return a value (which resolves the then
returned promise) or throw an error (which rejects the promise).In short then
calls internally obtain and record references to the resolve
and reject
functions of promises they return.
new MyPromise( 'p1')
works fine and is the first call to the subclass constructor.
.then( someFunction)
records someFunction
in a list of then
calls made on new MyPromise
(recall then
can be called multiple times) and attempts to create a return promise by calling
new MyPromise( (resolve, reject) => ... /* store resolve reject references */
This is the second call to the subclass constructor coming from then
code. The constructor is expected to (and does) return synchronously.
On return from creating the promise to return, the .then
method makes an integrity check to see if the resolve
and reject
functions it needs for later use are in fact functions. They should have been stored (in a list) along with callbacks provided in the then
call.
In the case of MyPromise
they are not. The executor passed by then
, to MyPromise
, is not even called. So then
method code throws a type error "Promise resolve or reject function is not callable" - it has no means of resolving or rejecting the promise it is supposed to return.
When creating a subclass of Promise, the subclass constructor must take an executor function as its first argument, and call the executor with real resolve
and reject
functional arguments. This is internally required by then
method code.
Doing something intricate with MyPromise
, perhaps checking the first parameter to see if it is a function and calling it as an executor if it is, may be feasible but is outside the scope of this answer! For the code shown, writing a factory/library function may be simpler:
function namedDelay(name, delay=1000, value=1) {
var promise = new Promise( (resolve,reject) => {
setTimeout(() => {
resolve(value)
}, delay)
}
);
promise.name = name;
return promise;
}
namedDelay( 'p1')
.then(result => {
console.log('fulfilled, result: ', result)
})
.catch(err => {
console.error('err: ', err)
})
;TLDR
The class extension to Promise is not an extension. If it were it would need to implement the Promise interface and take an executor function as first parameter. You could use a factory function to return a Promise which is resolved asynchronously (as above), or hack the posted code with
MyPromise.prototype.constructor = Promise
which causes .then
to return a regular Promise object. The hack itself refutes the idea that a class extension is taking place.
Promise Extension Example
The following example shows a basic Promise extension that adds properties supplied to the constructor. Of note:
The Symbol.toString
getter only affects the output of converting an instance to a string. It does not change "Promise" to "MyPromise" when logging an instance object on browser consoles tested.
Firefox 89 (Proton) is not reporting own properties of extended instances while Chrome does - the reason test code below logs instance properties by name.
class MyPromise extends Promise {
constructor(exec, props) {
if( typeof exec != "function") {
throw TypeError( "new MyPromise(executor, props): an executor function is required");
}
super((resolve, reject) => exec(resolve,reject));
if( props) {
Object.assign( this, props);
}
}
get [Symbol.toStringTag]() {
return 'MyPromise';
}
}
// Test the extension:
const p1 = new MyPromise( (resolve, reject) =>
resolve(42),
{id: "p1", bark: ()=>console.log("woof") });
console.log( "p1 is a %s object", p1.constructor.name);
console.log( "p1.toString() = %s", p1.toString());
console.log( "p1.id = '%s'", p1.id);
console.log( "p1 says:"); p1.bark();
const pThen = p1.then(data=>data);
console.log( "p1.then() returns a %s object", pThen.constructor.name);
let pAll = MyPromise.all([Promise.resolve(39)]);
console.log( "MyPromise.all returns a %s object", pAll.constructor.name);
try { new MyPromise(); }
catch(err) {
console.log( "new MyPromise() threw: '%s'", err.message);
}
Upvotes: 24
Reputation: 71
The post by asdru
contains the correct answer, but also contains an approach (constructor hack) that should be discouraged.
The constructor hack checks if the constructor argument is a function. This is not the way to go as the ECMAScript design contains a specific mechanism for sub-classing Promises via Symbol.species
.
asdru
's comment on using Symbol.species
is correct. See the explanation in the current ECMAScript specification:
Promise prototype methods normally use their this value's constructor to create a derived object. However, a subclass constructor may over-ride that default behaviour by redefining its @@species property.
The specification (indirectly) refers to this note in the sections on finally
and then
(look for mentions of SpeciesConstructor
).
By returning Promise
as the species constructor, the problems that traktor
's answer analyzes so clearly are avoided. then
calls the Promise
constructor, but not the sub-classed MyPromise
constructor. The MyPromise
constructor is called only once with the name
argument and no further argument-checking logic is needed or appropriate.
Therefore, the code should simply be:
class MyPromise extends Promise {
constructor(name) {
super((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 1000)
})
this.name = name
}
static get [Symbol.species]() {
return Promise;
}
get [Symbol.toStringTag]() {
return 'MyPromise';
}
}
Less is more!
Some notes:
MDN has an example for the use of the species symbol in extending Array
.
The most recent browser versions (Chrome, FF, Safari, Edge on MAC and Linux) handle this correctly, but I have no information on other browsers or legacy versions.
Symbol.toStringTag
is a very nice touch, but not required. Most browsers use the value returned for this symbol to identify the sub-classed promise in the console, but, beware, FF does not - this could easily be confusing. In all browsers, however, new MyPromise('mine').toString()
yields "[object MyPromise]"
.
All of this is also unproblematic if you author in Typescript.
As noseratio
points out, a primary use-case for extending Promises is the wrapping of (legacy) APIs that support abort or cancel logic (FileReader, fetch, ...).
Upvotes: 8
Reputation: 1264
The best way I found to extend a promise is
class MyPromise extends Promise {
constructor(name) {
// needed for MyPromise.race/all ecc
if(name instanceof Function){
return super(name)
}
super((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 1000)
})
this.name = name
}
// you can also use Symbol.species in order to
// return a Promise for then/catch/finally
static get [Symbol.species]() {
return Promise;
}
// Promise overrides his Symbol.toStringTag
get [Symbol.toStringTag]() {
return 'MyPromise';
}
}
new MyPromise('p1')
.then(result => {
console.log('resolved, result: ', result)
})
.catch(err => {
console.error('err: ', err)
})
Upvotes: 12