Sorry for the somewhat convoluted title.
I have been using Angular 1.x for a couple years now and have recently been investigating the potential of using TypeScript in a codebase I'm refactoring (also in Angular 1.x). Currently I'm having trouble thinking my way through making injectable dependencies of a class (and class families), rather than dependencies of constructor functions.
Angular has a strong bias towards building applications out of singleton services, but I've found these singleton services can often become contaminated with outdated/incorrect state over the lifetime of the application runtime, or create an unnecessary division between the functionality exposed by the services and the data it operates on.
As a consequence, one pattern I have strongly favored is to use Angular's .factory()
to generate constructor functions for use at various places in my applications. Within the context of ES5 and Angular's module system, you can do something like, e.g.:
angular.module('SomeApp', [])
.factory('BaseClass', baseClassFactory)
.factory('SubClass', subClassFactory);
baseClassFactory.$inject = ['$http'];
function baseClassFactory ($http) {
function BaseClass () {}
BaseClass.prototype = {
$http: $http
/* some additional implementation */
};
return BaseClass;
}
subClassFactory.$inject = ['BaseClass'];
function subClassFactory (BaseClass) {
function SubClass () {}
SubClass.prototype = Object.create(BaseClass.prototype);
angular.extend(SubClass.prototype, {
someNewMethod: function () {
this.$http.get('/some/url').then(function (data) {
/* some operation on the data to update this instance */
});
}
});
return SubClass;
}
The salient point being I now can make a family of "classes" that each have access to injected dependencies by merit of the initialization logic in my factory functions. Factories for child classes explicitly inject the results of factories for parent classes; we're forced to leverage Angular's module and dI system to accomplish this, but in the absence of something else, it works great.
In the example above I might be making a Model class whose instances expose raw data along with operations to save/delete/what-have-you using $http
. Elsewhere in my app I need merely inject one of my classes and I can new SubClass(someArgs).someNewMethod()
without a hitch.
In TypeScript (or, I imagine, plain-jane ES6 JS) this kind of pattern, which depends on runtime dependency resolution of Angular's modules and injectable components, is a duplication of effort from using TS/ES6 module system and inheritance.
For singleton services, or any other class where we expect dependencies to be injected to constructor functions, there is no problem. Indeed, the internet is abound with examples like:
class MyThing {
static $inject = ['$http'];
constructor(private $http: ng.IHttpService) {}
}
angular.module('asdf', []).service('myThing', MyThing);
This works great, even for subclasses of MyThing
.
However, what I have not found a good way of doing, is achieving the effect of my original JS example. Ideally I would be able to declare some classes that are meant to have many instances in the app, but which nevertheless have injectable dependencies. Something like the following:
class BaseClass {
$http: ng.IHttpService;
constructor(not, injectables) {
/* i do not exist to be injected */
}
}
class SubClass extends BaseClass {
someMethod() {
return this.$http.get('/some/url');
}
}
that makes the required $http
available on all instances of BaseClass
, along with all instances subclasses of BaseClass
without the tedium of doing:
angular.module('myModule', [])
.factory('BaseClass', baseClassFactory)
.factory('SubClass', subClassFactory)
baseClassFactory.$inject = ['$http'];
function baseClassFactory($http) {
BaseClass.prototype.$http = $http;
return BaseClass;
}
subClassFactory.$inject = ['$http'];
function subClassFactory($http) {
SubClass.prototype.$http = $http;
return SubClass;
}
This works, of course, but is a lot of extra (error prone) boilerplate. It also requires the author of any child classes to be keenly aware of each dependency specified by the parent.
I hope this made sense. Does anyone have any suggestions for how to approach this problem?
I believe it should be treated in Typescript in similar manner like it would be in ES7:
This way children may inherit dependencies or have their own by calling
super()
.