How TypeScript Decorators can make your life as a developer more pleasant
TypeScript provides a built-in implementation of Decorators. If you already recognize them from your day-to-day work, you might still be curious whether there's more to discover. A decorator is a function that you place above a class, method, property, parameter, or accessor using the @ sign, in order to give that declaration extra behavior or metadata. Decorators receive context about their target, so they can read, register, or modify things without polluting the core logic. This lets you write declarative, readable, compact annotations that spare you from wading through endless boilerplate before you can even start on your logic.
More important than knowing how they work is knowing when it's worth developing and implementing them yourself - hence this article.
Problem
Picture the following scenario: you're developing an application that performs a slow and complex calculation. Good news — you spot the opportunity to speed it up through memoization. Memoization is a technique where we cache the results of expensive operations according to their input parameters. You gain speed for deterministic functions that are called repeatedly.
To demonstrate the value of your work, you're going to do two things: you want to measure how long each calculation takes with logging, and you want to add memoization to save time.
Code that simulates the problem
heavyCalculation is the method to which we want to add our memoization and logging. To keep our example clear, I use the sleep method, which resolves a promise after 3 seconds. This lets us concentrate on adding our logging and memoization.
Solving the problem without decorators
Here, logging and memoization were added inside our method. Everything is now intertwined, and you basically have to read the entire method to see what's going on. Suppose we want to reuse this code for another slow method - then we'd have to disentangle all of this again.
Decorators as the solution
By abstracting our code into decorators, we get reusable and readable code.
Why?
For developers (and for your future self) we like to write readable/reusable code so that we can fully concentrate on the features our users enjoy. With the arrival of classes in TypeScript came the need to annotate and extend classes and class members - which is made possible through Decorators.
At a glance you can see that someone once saw the need to give the complexSlowProcess method a decorator. Without the decorator, a developer would have to work out the full logic in a separate function that gets called within the complexSlowProcess method. That way, the method gets "polluted" with logic that isn't actually the purpose of the method.
Where to apply them?
- class definition
- properties
- methods
- accessors
- parameters
As soon as you're working with TypeScript classes, you can get started.
Decorator Factory
A decorator factory is a function that returns an expression which will be called by the decorator. This makes it possible to pass configuration to your decorator.
Composition
The scenario where you want to combine multiple decorators comes up quickly. As mentioned earlier, this is where you need to pay attention - you already saw an example of it under "Decorators as the solution," where logDuration and memoize are combined. Take these two decorator factories:
Applied in this order:
They produce this in the console:
The expression for each decorator is evaluated from top to bottom, after which the results of each decorator are called from bottom to top, and finally the method itself.
It's tempting, when composing, to write your decorators in such a way that they become intertwined and can no longer function without each other. Watch out for this! A decorator should always be able to function on its own - that way you keep clean code with separation of concerns.
Decorator design pattern
A decorator is a function that wraps around our own code and enriches it in some way. It becomes possible to strip abstractions out of our business logic. Decorators are composable and relieve their target of complexity.
As with every design pattern, there are pros and cons.
Advantages
- Extending functionality modularly without subclassing.
- Combinable behavior with multiple decorators.
- The code in our class can concentrate on its own purpose without digressing. Our class thus adheres to the single responsibility principle.
- Because decorators always sit in front of the code they apply to, you can see at a glance which extra functionality is being applied. Readability and maintainability are essential for good code.
Disadvantages
- The order of decorators can matter. When you start combining them, you have to be careful.
- Removing a decorator later can become a tricky puzzle because of the ordering. It happens fast: you combine a number of decorators on a method, later you extend their functionality, and before you know it they can no longer work without one another.
Examples
The decorator pattern is often used in the following cases:
- Logging arguments & return values
- Performance logging
- Authentication & authorisation
- Validation
- Caching
- Memoization
- Event handling
- Retry mechanisms
If you want to provide one of these, I wouldn't hesitate. Perhaps you'll want to combine them too, and then it quickly becomes clear why it's nice to have code that's readable and reusable.
My concrete experience comes from Angular, but also from a project where enormous gains were made with a decorator that does memoizing. The application uses a library to perform heavier mathematical calculations for drawing NURBS. These NURBS are drawn with three.js. What you draw in three.js only becomes visible once you assign a material to a geometry - and here too, the materials that get created are reused via a memoize decorator.
Initially, I had really underestimated the impact of the decorator, until the performance issues surfaced. Fortunately, the decorator was super easy to apply!
Conclusion
Through decorators, we extend the existing behavior of our classes, methods, properties, parameters, and accessors without losing focus on the purpose of our class. Decorators are declarative, modular, and eye-catching. With the knowledge in this article, you as a developer can already sense whether your project would benefit from them.