When working with component-based frameworks, we tend to favor Composition over Inheritance because of the flexibility that Composition provides. This is especially true in Angular due to Dependency Injection and how Inheritance in JavaScript works.
Take a look at the following class:
This is how one would construct an abstract class
in Angular and set up Dependencies for that class. Sub-classes can extends
the base abstract class as follow:
Thanks to how Angular resolves Dependency Injection, Concrete
class automatically receives those Injectables assuming that:
Concrete
does not need extra injectablesConcrete
constructor is expected to be the same asInstance
constructor
The above constraints are limiting to what we can do with Instance
’s sub-classes. What if we have extra logic in the sub-class constructor? What if we need to inject more Injectables into the sub-class? Let’s see an example
Duplicating code is one thing. One important aspect is that now ConcreteTwo
, or any sub-class of Instance
, needs to adjust its constructor AND tests if Instance
needs more Injectables in the future. That is painful 😖
We can convert the Inheritance here to Composition by bringing the logic in Instance
to a Service instead
Then use the InstanceService
as follow
This way if InstanceService
changes in the future, Concrete
might not need to change its code or tests. However, there are some caveats:
- Services do not have
@Input()
and@Output()
, what if ourInstance
base class has some common inputs and outputs? There are ways to achieve this but it is verbose, error-prone, and repetitive. Eg: We can have Subjects in the Service and use Setter Inputs to push data through those Subjects - Because Services do not have
ngOnInit
life-cycle, ALL components that injectInstanceService
need to manually callinit()
method
Well, Angular 14 comes with a fix that maybe, just maybe, makes Inheritance in Angular viable again
inject()
Here’s the official documentation on inject()
. TL;DR, when we create InjectionToken
, we can define some sort of default value for said InjectionToken
with the following syntax
Then we can start using MY_TOKEN
with @Inject(MY_TOKEN)
, if we do not provide MY_TOKEN
anywhere in our Injector tree, then the value we define in factory()
will be used. Then comes this PR: https://github.com/angular/angular/pull/45991
The PR makes it possible to use inject()
in a component’s constructor. One might think “It’s not a big deal” but based on the example above, we see that it is. Let’s rewrite the Instance
base class:
Then our Concrete
This fixes our issue above 🤯! What’s more? Instance
can have Inputs, Outputs, and it can have life-cycle hooks like ngOnInit
I get very excited because I use Inheritance for Angular Three and cannot wait to make this big refactor that reduces the code and complexity by a ton. I hope that you all can find ways to utilize this change to inject()
in Angular 14 as well.
This is not a blog to promote Inheritance over Composition. This is merely stating the use-case that I run into and how Inheritance helps solve it easier than Composition. There might be other caveats about testability with inject()
. If you’re interested and curious, hit me up on Twitter and I am happy to jump in a call and show you the exact use case.
Have fun and good luck!