2

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?

Community
  • 1
  • 1
tobuslieven
  • 1,474
  • 2
  • 14
  • 21
  • 3
    Can you expand on "it can't be done with the standard old syntax" – tommybananas Jan 23 '17 at 21:42
  • 2
    Perhaps you could even link to the questions where you were told it couldn't be done. Note that in a number of programming languages, the "I" prefix is used to denote an interface rather than concrete class. The naming in the question may be confusing at first glance for those people. – Heretic Monkey Jan 23 '17 at 21:45
  • 1
    @tommybananas Since it's a native language type, it can only be extended with `class` syntax (hence no ES5), just like you can't extend `Array` or `Error` without hacks using `__proto__`. – loganfsmyth Jan 23 '17 at 21:54
  • @MikeMcCaughan I added the link to the other question [Extending a Promise in javascript](http://stackoverflow.com/questions/41792036/extending-a-promise-in-javascript) now. That's a useful tip about the "I" prefix. – tobuslieven Jan 23 '17 at 22:13
  • @loganfsmyth It seems a bit weird that it can't be made extendible by the core language implementors but a more or less extendible version can be made ad hoc as above. Why didn't they just make the builtin work like normal? – tobuslieven Jan 23 '17 at 22:17
  • @tommybananas It seems to throw a TypeError when I try to use the Promise constructor to populate my DerivedPromise something like: `function DerivedPromise(executor) { Promise.call(this, executor); }` gives TypeError. Here's the original question [Extending a Promise in javascript](http://stackoverflow.com/questions/41792036/extending-a-promise-in-javascript). – tobuslieven Jan 23 '17 at 22:25
  • 1
    When you chain these, you won't get an `IPromise` object after chaining because your `iPromise.prototype.then()` just returns a normal promise with this `return this.promise.then(success, fail);`. – jfriend00 Jan 23 '17 at 23:04
  • @jfriend00 That's one of the main things I was wondering about. Would it make sense for IPromise.then to do something like: `this.promise.then(success, fail); return new IPromise(whatGoesHere?);` or `this.promise.then(success, fail); return new this.constructor(whatGoesHere?);` I know that Promise.then returns a Promise, but I'm not sure how that Promise is setup. Could IPromise.then even do this: `this.promise.then(success, fail); return this;`? If you could put that in an answer it would be awesome. – tobuslieven Jan 24 '17 at 09:47
  • 1
    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](https://kangax.github.io/compat-table/es6/#Promise_is_subclassable) 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. – jfriend00 Jan 24 '17 at 09:56
  • 1
    Something something `Symbol.species` something something assimilation, something something - don't do this and just directly subclass `Promise`. – Benjamin Gruenbaum Jan 24 '17 at 10:18
  • @BenjaminGruenbaum But I don't want to use the ES6 class syntax (I think that's the only way to directly subclass Promise). I feel like it might be hiding what's really going on and I want to know that stuff. Symbol.species seems interesting, but not 100% sure how it would help here. Honestly I think some of these comments deserve to be answers, I'd love to see more about your idea. – tobuslieven Jan 24 '17 at 10:32
  • @jfriend00 I'm going to turn your comments into an answer (with credit ofcourse) if that's ok with you. They point out one of the main differences between IPromise and Promise, which is exactly what I wanted to see answers about. – tobuslieven Jan 24 '17 at 10:39
  • 1
    In my opinion - don't subclass promise. I've seen it done a lot of times but I have not once seen it done correctly. Compose promises instead. – Benjamin Gruenbaum Jan 24 '17 at 11:45
  • @BenjaminGruenbaum I agree, but isn't that kinda what I'm doing? Composing a promise to create a new class then inheriting from it. All the inheriting does is that in your new class you don't have to write a new MyClass.prototype.then method to expose the Promise's chaining behaviour. As long as you're aware that `then` is going to return a normal Promise, you're good to go. – tobuslieven Jan 24 '17 at 13:00

2 Answers2

2

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
}
Oriol
  • 274,082
  • 63
  • 437
  • 513
0

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.

tobuslieven
  • 1,474
  • 2
  • 14
  • 21