Angular 2, decorators and class inheritance
In Angular 2, decorators allow developers to configure classes as particular elements by setting metadata on them. The most commons are the @Component one, for components and the @Injectable one, for classes you want to inject dependencies in.
We don’t provide a comprehensive description here regarding the differences between decorators and annotations. Pascal Precht published a great article on this:
That being said, when implementing big applications, developers try to minimize the code duplication and look for solutions to gather common configuration into parent classes. They leverage then class inheritance.
Imagine you want to reuse an existing component but only change its selector or its styles or reuse templates, the component code remaining the same. Right now, you need to duplicate the code.
What about such a support in Angular 2?
In fact this feature won’t be implemented in Angular 2 because it’s difficult to provide a comprehensive and generic support.
This has been discussed in depth before, and the conclusion is that it is not clear what does it mean to inherit decorators / annotations. Yes we could make a set of rules, but it would be rather arbitrary, and than we would have to define a set of merge rules, (how do you merge parent and child properties on the decorator). While this sounds like a great idea at first, the more you dig into it the more arbitrary it becomes as to how to handle corner cases is. For this reason, we will not support this.
Rob Wormald also provides some hints for such an issue:
@Decorators are just functions (so inheritance doesn’t really make sense)
You can compose them relatively easily (that is, wrap them in a function that itself is a decorator) but I’d advise against this, as it’s possible this will interfere with build tooling that looks for @Component decorators.
I do understand the arguments re: DRY, but in general angular 2 errs in favor of explicitness.
What do decorators actually do in Angular 2?
Basically, decorators in Angular 2 apply metadata on classes leveraging the Reflect Metadata library. When Angular 2 will use the class, it will get this metadata to configure the expected behavior, for example in the case of a component.
Angular 2 uses several entries for this metadata:
- annotations. This corresponds to metadata set by decorators at the class level. It’s an array since you can apply several decorators at this level. For example, @Component and @Routes.
- design:paramtypes. This corresponds to the types of constructor parameters. It only applies for TypeScript since with ES6, such parameters aren’t supported. With this language, you need to supply a static getter for the parameters property.
- propMetadata. This corresponds to metadata set by decorators at the class property level. It’s an object and each entry name is the property name. Each entry contains an array since it’s also possible to define several decorators on a property.
- parameters. This corresponds to metadata set by decorators at the constructor parameter level. It’s an array of arrays since it’s always possible to define several decorators on a parameter.
Note that desig:paramtypes and parameters are used by dependency injection to determine what to inject.
This figure summarizes links between classes, decorators and metadata:
Let’s take a concrete sample. Imagine that you have implemented the following component:
Here is how metadata are set on the corresponding class:
In the case of a service with the following code:
You will have something similar:
Support of base components
Now we saw the foundations and how Angular 2 leverages decorators and metadata, we can deal with the inheritance support.
It’s obvious that classes can’t inherit automatically metadata from parent classes. In short, class inheritance in TypeScript means that you can have access to properties and methods defined in the parent class.
Leveraging annotations of the parent class must be done manually. The thing consists of referencing the parent class. Annotations are set on the associated parent constructor function. Don’t forget that ES6 / TypeScript classes are “just” a sugar syntax for constructor functions and prototypes.
Within decorators, the target class (i.e. the associated constructor function the decorator is applied on) is provided as a parameter. To get the constructor function of the parent class, we need to follow the prototype chain and get the associated constructor.
Here is a sample:
We are now able to access the metadata from the parent class of the decorator one using Reflect Metadata. Notice that the parent class must be also decorated to have metadata. The following figure describes this:
Here is the code to access metadata from the parent class:
Now we need to implement how you will build the metadata of the child component from the parent ones and the input parameter of the child decorator. This is what Miško refers to when he says “set of merge rules”.
We choose here to override what was defined in the parent metadata if something is specified at the child level. We then set this metadata on the child class. Here is a sample:
This decorator can be used as described below. You can see that the selector is overridden in the child component and this one inherits the template.
You could also want to build the value of a metadata attribute from the corresponding one in the parent class. For this, we need to support the use of functions for attributes. Here is a sample:
Such a support can be used this way:
Leveraging parent classes for dependency injection
Another issue of decorator / metadata and inheritance is dependency injection. Imagine you want to specify in a parent class all the elements you want to inject in its subclasses. This way you wouldn’t need to define this in each subclass.
In a similar way that what we did for the custom Component decorator, we can leverage metadata related to dependency injection from the parent class:
We can use this decorator as described below. The thing to notice is that it’s necessary to define a constructor in the child class that calls the parent one. The call needs to leverage the implicit arguments variable for injected objects.
You can notice that such processing could also add to the previous custom Component decorator.
What is a bit tricky here is the ability to specify additional elements to inject in the child class.
After having described the issue of decorators, metadata, and class inheritance, we understand better why Angular 2 doesn’t support such a feature.
That being said, it also appears to be an important feature since it allows developers not to duplicate code regarding components and services when inheritance is involved.
The following plunkr describes how to print the metadata set by Angular 2 for both component and service:
The following plunkr describes the way to implement the @CustomComponent decorator:
The following plunkr describes the way to mix dependency injection configuration and class inheritance with the @CustomInjectable decorator: