Search code examples
javascriptclasstypescriptpartial-classes

How can I use Javascript "partial classes" with TypeScript style class definition


My application has an api wrapper class originally created by typing it out in TypeScript and copying/pasting the javascript into my app.

So the class def looks like this:

var SiteApi = (function () {
  function SiteApi(initially) 
  {
    //stuff
  }

  SiteApi.prototype.method1 = function(){/*stuff*/};
  SiteApi.prototype.method2 = function(){/*stuff*/};

  return SiteApi;
})();

Now when they are on the admin page, I want to add an additional admin.js file that will contain admin methods. For example

SiteApi.prototype.Admin.method1 = function(){/*stuff*/};

I found an example that does the "end result" that I want:

// file main
function SomeObject() {
    for (var i = 0, ii = SomeObject.Partial.length; i < ii; i++) {
         SomeObject.Partial[i].apply(this, arguments);
    }
}

SomeObject.Partial.SomeName = function() {
   ...
}

// file extra
SomeObject.Partial.SomeOtherName = function() {
   ...
}

(from: Is it possible to give javascript partial class behavior like C# or monkey patching like Ruby does?)

However, the type of class definition they are using is different.

How can I keep the TypeScript style class definition and yet do something similar to this example to add on the admin functions?

For reference, we use our class like so:

siteApi = new SiteApi();

So I imagine there will also need to be a line of code tying the admin functions into it.

Note, I'm Ok with using something like SiteApi.admin_method1 but the issue is that with TypeScript style classes the prototypes are defined in the definition and the object is executed, so it doesn't seem straightforward how to add in more prototypes later.


Solution

  • Option 1

    Extend SiteApi but shadow the original, e.g. your admin.js loaded after would contain something like

    SiteApi = (function (old_SiteApi) {
        function SiteApi() {
            old_SiteApi.apply(this, arguments);
            // further construction
            this.admin_method1 = function () {/* some admin instance method */};
        }
        SiteApi.prototype = Object.create(old_SiteApi.prototype);
        // add more prototype things
        SiteApi.prototype.admin_method2 = function () {/* admin stuff through prototype */},
        return SiteApi;
    }(SiteApi));
    

    Option 2

    Let SiteApi know to expect more stuff later, e.g. the original definition would become

    var SiteApi = (function () {
        function SiteApi(initially) {
            //stuff
            var i;
            for (i = 0; i < SiteApi.Extras.length; ++i) {
                SiteApi.Extras[i].apply(this);
            }
        }
        SiteApi.Extras = [];
    
        SiteApi.prototype.method1 = function () {/* stuff */};
        SiteApi.prototype.method2 = function () {/* stuff */};
    
        return SiteApi;
    }());
    

    Then the admin.js would do

    SiteApi.Extras.push(
        function () {
            // stuff to make `this` into an Admin object
            this.admin_method1 = function () {/* some admin instance method */};
        }
    );
    

    Option 3

    The prototype of SiteApi is still exposed, if you don't need to do more construction and just need new methods you could simply add them in admin.js

    Object.assign(SiteApi.prototype, {
        admin_method1: function () {/* admin stuff through prototype */},
        admin_method2: function () {/* more admin stuff through prototype */}
    });
    

    This last option is the only one which will effect instances of SiteApi created before admin.js was loaded.

    Of course, you could mix options 2 and 3 together as well. Similarly, you could go with option 1 but call the new constructor a different name rather than shadowing the original, like AdminApi, and use that instead of SiteApi on the admin page.