Decorators can be used to inject class properties, aid in collaborative development by annotating issues at certain points in your codebase — whether at the class level or method level — or simply log method arguments and return values in a non-obstructive way.
Decorators follow the decorator design pattern, and are often referred to as a type of meta programming. In the realm of Javascript, Angular have heavily adopted decorators into the framework. We have also seen packages such as lodsah adopt decorators to easily apply its functions to your code using the feature.
Note: Python has a mature decorator implementation that is heavily adopted today — the Flask web server being a great example whereby decorators are used to bind an endpoint to a function.
To experiment with Typescript decorators, enable them in tsconfig.json
:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
Or on the command line:
tsc --target ES5 --experimentalDecorators
Let’s jump straight into decorators to familiarise ourselves with the overall concept, before getting stuck into the implementations further down the article.
You may have seen decorators being used in projects you have previously contributed to, noticing the @
symbol followed by a function name being applied to an object, class, class properties, or even method parameters.
And this is what can make decorators appear confusing; they are attached to a range of blocks of code, and where they are attached determines their implementation: In other words, your decorator function signatures will vary depending on what kind of object you attach them to. E.g:
class
will give it access to the class prototype and its member properties.class method
will give it access to the method’s parameters, the metadata associated with the method object, as well as its class prototype.class property
will give it access to the name and value of that property, along with its class prototype.class method parameter
will give it access to that parameter’s index, name and value.Concretely, we can attach decorators to:
// class definitions
@decorator
class MyComponent extends React.Component<Props, State> {
...
// class methods
@decorator
private handleFormSubmit() {
...
// class method parameters
private handleFormSubmit(@decorator myParam: string) {
...
// accessors
@decorator
public myAccessor() {
return this.privateProperty;
}
// class properties
@decorator
private static api_version: string;
Note: The above class definition example extends a React component. It is perfectly legal to use Typescript decorators with React and any other Javascript framework that Typescript can be applied to.
It is very unlikely that you will name your decorators @decorator
. Maybe you will run into a decorator with a name not dissimilar to the following:
...
@deprecated
class MyComponent extends React.Component<Props> {
...
}
In this case, a @deprecated
decorator has been applied to class MyComponent
. Decorator names should be readable, short and self explanitory. But what exactly can @deprecated
be used for?
It would be handy if we documented within our codebase which classes or methods are planned to be phased out or no longer supported, with the intent of being completely removed in the next milestone. In this scenario, new developers on-boarding themselves into the project will know straight away that it is not worth enhancing this class.
In addition, if an issue has been posted relating to the class, then developers will know to avoid @deprecated
classes, and could instead implement a new solution. Not only will the developer see @deprecated
next to these soon-to-be removed pieces of code, the implementation of @deprecated
could also console.log
a message to the console, notifying the developer if the class is mistakenly called at runtime. Very handy.
But right now we are not supplying any data to these decorators. What if we wanted to pass in arguments? We can do so by utilising the concept of Decorator Factories.
Let’s expand the above example by combining @deprecated
with another decorator, @inject
:
@inject({
api_version: '0.3.4'
})
@deprecated
class MyComponent extends React.Component<Props> {
...
}
As you can see, multiple decorators can be applied to one class, and each of which can accept arguments. The @inject
decorator is now accepting an api_version
argument, wrapped in an object.
But wait — we discussed above that a decorators’ signature is different depending on what we attach it to (class, method, property, etc…). Surely this argument hosting api_version
is not a part of this decorators’ signature — and yes, that is correct.
To get around this, we wrap the decorator implementation within another function (that supports our desired arguments) which simply returns the decorator implementation. These are known as Decorator Factories, and can be implemented like so:
function inject(options: { api_version: string })
return target => {
...
}
}@inject({
api_version: '0.3.4'
})
class MyComponent extends React.Component<Props> {
...
}
A lot has happened in the above example. Let’s break it down:
@inject
is simply referring to the inject(options)
function.inject(options)
returns the class decorator implementation, which just has one argument — target
. Target will give us access to the entire class prototype.Let’s complete this example by injecting the api_version
into the class as a static property, and therefore completing our decorator implementation:
function inject(options: { api_version: string })
return target => {
target.apiVersion = options.api_version;
}
}@inject({
api_version: '0.3.4'
})
class MyComponent extends React.Component<Props> {
static apiVersion: string;
...
}
This wraps up our first completed decorator! Injecting class properties is a useful way to add data to a class without disrupting its logic; In the above case I may need different classes to support specific API versions as to not break their implementations.
For the record, our @deprecated
decorator could be implemented in the following way, without a Decorator Factory:
function deprecated(target) {
console.log('this class is deprecated and will be removed in a future version of the app');
console.log('@: ', target);
}
@deprecated
class MyComponent extends React.Component<Props> {
...
Note: Try to _console.log(target)_
now within your decorator implementation to see how your class is represented in the console.
Now, we have only covered one type of decorator implementation — the class decorator whereby target
is the only parameter we have access to — but we will now want to familiarise ourselves will every implementation available to us. Let’s do that next.
Here we will document the definitions of each type of decorator.
We have just covered class decorator implementations, that provide the class prototype (target
) as the only parameter:
function classDecorator(options: any[]) {
return target => {
...
}
}
@classDecorator
class ...
Class property decorators also present the class prototype as the first parameter, but also supply the name
of the property we are accessing:
function prop(target, name) => {
...
}
}
class MyComponent extends React.Component<Props> {
@prop
public apiVersion: string
However, if the property is in fact a static property, the first parameter will present the class constructor function instead:
...
@prop
public static apiVersion: string;
...
Perhaps the most useful implementation, a method decorator will again provide us with the class prototype, but also two others: propertyKey
and propertyDescriptor
. Let’s use a Decorator Factory to represent this:
function methodDecorator(options: any[]) {
return (
target: MyComponent,
propertyKey: string,
propertyDescriptor: PropertyDescriptor
) => {
...
}
}
class MyComponent extends React.Component {
...
@methodDecorator
handleSomething() {
...
}
}
As the target
parameter will be our class prototype, we could type it as the class component the decorator will be applied to, in the above case, MyComponent
. The second parameter, propertyKey,
will be a string containing the name of the method: handleSomething
.
The third parameter — propertyDescriptor
— is particularly useful here; it provides us with standard metadata (MDN) associated with the object: configurable
, enumerable
, value
and writable
, as well as get
and set
. We can overwrite any of these values if we intended to modify this function.
For example, if I wanted to toggle enumerability I could create the following decorator, providing a boolean in the Decorator Factory to determine enumerability which will then be applied to propertyDescriptor.enumerable
:
function enumerable(enumerable: boolean) {
return (
target: MyComponent,
propertyKey: string,
propertyDescriptor: PropertyDescriptor
) => {
propertyDescriptor.enumerable = enumerable;
}
}
class MyComponent extends React.Component {
...
@enumerable(false)
handleSomething() {
...
}
}
This is great and demonstrates the power of decorators, but a (frankly) more interesting use case is to be able to log method arguments as they are called, as well as the returning value of that method. This gives us a comprehensive breakdown of what is happening at runtime, acting as a valuable debugging tool.
To do this we can refer to the original method definition using propertyDescriptor.value
, before applying it to a new definition, adding our logging before and after the original:
function logger(
target: MyComponent,
propertyKey: string,
propertyDescriptor: PropertyDescriptor
) => {
//get original method
let originalMethod = propertyDescriptor.value;
//redefine descriptor value within own function block
propertyDescriptor.value = function (...args: any[]) {
//log arguments before original function
console.log(
`${propertyKey} method called with args:
${JSON.stringify(args)}`);
//attach original method implementation
let result = originalMethod.apply(this, args);
//log result of method
console.log(
`${propertyKey} method return value:
${JSON.stringify(result)}`);
}
}
//apply decorator to a class method
...
@logger
handleSomething(name, value) {
...
}
In the above example we have used apply()
to re-attach the original method to our new definition, with logging included.
Now moving onto implementing method parameter decorators, we have access to the name and index of the parameters we decorate:
function decorator(
class,
name: string,
index: int
) => {
...
}class MyComponent extends React.Component<Props> {
...
private handleMethod(@decorator param1: string) {
...
}
}
Again, this is useful for logging how arguments are passed and to which parameter they are associated with. They can also be used to amend arguments where you may want to modify the values being passed through, e.g. a @lowercase
or @uppercase
decorator.
For numbers, in the event you wish all your currency values to adhere to 2 decimal places, you could apply a @rounded
decorator to these params as an alternative to polluting your boilerplate with primitive Javascript functions.
This sums up the current state of decorator implementations. At this stage you may be forming an idea of where you’d like decorators applied — is it worth to introduce additional complexity to your method signatures with parameter decorators? Or would you lean towards only using decorators to modify methods only — this will boil down to either personal taste or a collective team decision from project to project.
Keep your project structure intact by separating decorator functions from your class implementations, and import them via ES6 syntax:
import { logger, deprecated } from '../decorators';
Your decorator files should sit in src/
within a dedicated decorators/
folder:
src/
/decorators
index.ts
You may also wish to export a set of decorators into an npm package and use the package throughout your projects. This, in my experience, is the best way of managing decorators throughout a multi-project setup, scoping the package under your organisation:
import { logger, deprecated } from '@myorg/decorators';
By now you should be able to implement decorators within your own Typescript projects.
In my opinion it is 100% worth taking some time to think about how decorators can be applied to your projects. We have already seen the benefits of decorators in other languages, and will undoubtedly be a part of Javascript and Typescript for the foreseeable future.
Although the underlying implementation of decorators may change as we come closer to a final Javascript proposal, most of these changes will be under the hood; the syntax adopted in this article will most likely stay the same — developers are now accustomed to this pattern, with other frameworks and languages adopting the same syntax rules.
As an open-source contributor I envisage decorators being used for Github related use-cases, such as issue tracking — for example, with an @issue
decorator. Instead of browsing Github issues in the browser before relating them to the project codebase, it would be quicker and easier for developers to simply open the console and have a list of issues popping up with a URL to the Github issue, along with their related class or method, and file path to the file in question. Of course, you could have a boolean constant, const LOG_ISSUES = true
, to toggle console output to any decorator.
Likewise, having the ability to browse a project and notice something like this directly in your IDE will give further clarity on what needs to be worked on:
@issue(850, 'good first issue')
class ProblematicClass extends React.Component<Props, State> {
...
Beyond collaborative development, utility decorators such as logging how long a method takes to process may be useful for API calls. A @memoize
decorator would then be useful to cache resulting API results for future method calls.
Decorators definitely have their place in Typescript, and developers accustomed to them from elsewhere will undoubtedly be interested in using decorators in Javascript projects. For newcomers, decorators will offer an alternative design pattern as well as an opportunity to re-think how certain meta-progamming tasks are done in their projects.
#Typescript #Angular #WebDev