tobuslieven
tobuslieven

Reputation: 1534

Is this an inheritable Promise?

Just for fun/learning I wanted to extend Promise and found, via incredibly knowledgable people on stackoverflow, that it can't be done with the standard old syntax. But I wanted to anyway, so I thought of creating my own IPromise class that composes a Promise object and allows others to inherit from it using the old non ES6 syntax.

I want to know how this implementation differs from a universe where the builtin Promise is directly inheritable, and if anyone has insight, why the implementors of the builtin Promise didn't allow inheritance with the old syntax.

Here's my extendible/inheritable IPromise class:

// Inheritable Promise
IPromise = function(executor) {
    this.promise = new Promise(executor);
};
IPromise.prototype = Object.create(Promise.prototype);
IPromise.prototype.constructor = IPromise;
IPromise.prototype.catch = function(fail) {
    return this.promise.catch(fail);
}
IPromise.prototype.then = function(success, fail) {
    return this.promise.then(success, fail);
};  

// Usage
// No different to builtin Promsise
(new IPromise(function(resolve, reject) { return resolve(true); }))
    .then(function(response) {
        console.log('IPromise then is like Promise then', response);
    })
    .catch(function(error) {
        console.log('IPromise catch is like Promise catch', error);
    });

Here's an example of extending it for a batch ajax that waits for all requests to complete regardless of whether any of them fail. Slightly different to builtin functionality.

// Inheriting
// You can inherit from IPromise like you would any normal class.
BatchAjax = function(queries) {
    var batchAjax = this;
    this.queries = queries;
    this.responses = []; 
    this.errorCount = 0; 
    IPromise.call(this, function(resolve, reject) { 
        batchAjax.executor(resolve, reject);
    });
};
BatchAjax.prototype = Object.create(IPromise.prototype);
BatchAjax.prototype.constructor = BatchAjax;
BatchAjax.prototype.executor = function(resolve, reject) {
    var batchAjax = this;
    $.each(this.queries, function(index) {
        var query = this;
        query.success = function (result) { 
            batchAjax.processResult(result, index, resolve, reject);
        };
        query.error = function (jqXhr, textStatus, errorThrown) {
            batchAjax.errorCount++;
            var result = {
                jqXhr: jqXhr, textStatus: textStatus, errorThrown: errorThrown
            };
            batchAjax.processResult(result, index, resolve, reject);
        };
        $.ajax(query);
    });
};
BatchAjax.prototype.processResult = function(result, index, resolve, reject) {
    this.responses[index] = result;
    if (this.responses.length === this.queries.length) {
        if (this.errorCount === 0) {
            resolve(this.responses);
        } else {
            reject(this.responses);
        }
    }
};

// Usage
// Inheriting from IPromise is boring, which is good.
var baseUrl = 'https://jsonplaceholder.typicode.com';
(new BatchAjax([{url: baseUrl + '/todos/4'}, {url: baseUrl + '/todos/5'}]))
    .then(function(response) {console.log('Yay! ', response);})
    .catch(function(error) {console.log('Aww! ', error);});

Bear in mind that I'm just learning and this isn't meant to be useful, just interesting. But feel free to give brutal criticism, I'm open to the idea that this is a stupid thing to do : )

Cheers for taking a look and sorry for all the code!

edit: Here's the question where I was originally trying to extend the promise: Extending a Promise in javascript. It doesn't seem to work with the old syntax because the Promise constructor throws a Type error if it notices that it's initialising something that's not a Promise (I think).

edit2: jfriend00's comment highlighted something interesting. What should IPromise.then return? At the moment it's just a Promise, but should it be an IPromise?

IPromise.prototype.then = function(success, fail) {
    this.promise.then(success, fail);
    return new IPromise(whatGoesHere?);
};

Or an instance of whatever class has inherited from it.

IPromise.prototype.then = function(success, fail) {
    this.promise.then(success, fail);
    return new this.constructor(whatGoesHere?);
};

Or could it just return this?

IPromise.prototype.then = function(success, fail) {
    this.promise.then(success, fail);
    return this;
};

I know Promise.then returns a Promise, but I don't know how that Promise is setup or expected to behave. That's why I avoided the issue and just returned the Promise from this.promise.then. Are there any other sensible solutions?

Upvotes: 2

Views: 289

Answers (2)

tobuslieven
tobuslieven

Reputation: 1534

The following was suggested by jfriend00's comments.

When you chain IPromises, you won't get an IPromise object after chaining because your IPromise.prototype.then() just returns a normal Promise with return this.promise.then(success, fail);

Me: Should IPromise.then just do:

this.promise.then(success, fail);

And then one of these?

return new IPromise(whatGoesHere?);
return new this.constructor(whatGoesHere?);
return this;

This is one of the reasons that it's really messy to extend promises. The infrastructure itself creates new promises with every .then() call and does not provide a simple way for you to hook into that without the underlying infrastructure supporting subclassing. If you look here that support is there in some environments. But, I've always found I could solve my problems in other ways. So, I'm not going to try to hack my own subclassing as it seems likely to have holes in it.

Back to me (tobuslieven) talking again now. I agree with this but I'm still going to keep this class in mind and use it if I see an opportunity. I think returning Promise instead of IPromise is workable and it's really fun to be able to extend Promises like this.

So my answer is Yes, possibly good enough and worth having a go at using for real somewhere.

Upvotes: 0

Oriol
Oriol

Reputation: 288010

Yes, your IPromises behave mostly like real Promise. But for example, you cannot call native promise methods on them:

var p = new Promise(function(resolve) { return resolve(true); });
var ip = new IPromise(function(resolve) { return resolve(true); });
Promise.prototype.then.call(p, v => console.log(v));
Promise.prototype.then.call(ip, v => console.log(v)); // TypeError

If you want your IPromise instances to be real promises, they must be initialized by Promise. That constructor adds some internal slots like [[PromiseState]] which you cannot emulate. But promises created by Promise inherit from Promise.prototype, and before ES6 there is no standard way to change the [[Prototype]] to IPromise.prototype after the object has been created.

Using ES6 classes,

class IPromise extends Promise {
  // You can add your own methods here
}

Upvotes: 2

Related Questions