Mastering TypeScript Decorators: A Comprehensive Guide to Enhancing Your Code
date
Oct 4, 2023
status
Published
tags
Typescript
summary
type
Post
Why do we need a decorator?
If you’re new to TypeScript decorators, you may be wondering why they’re necessary. Before we delve into the answer, let’s take a look at a small code snippet.
a class and method without a typescript decorator
The
getHomeAddress
function violates the single responsibility principle because it performs multiple tasks that should be handled by separate functions. Specifically, the function is responsible for validating parameters, authorizing users, executing business logic, and handling errors. Although the primary purpose of the function is to call homeAddressApi
and return the result, it also performs these additional tasks, which makes the function more complex and harder to maintain.A better approach would be to delegate the validation, authorization, and error-handling logic to separate functions, allowing getHomeAddress to focus solely on the business logic of retrieving home addresses.
By using a TypeScript decorator, we can transform the original code into a more streamlined version that separates concerns and improves code maintainability.
a better method with typescript decorator
By using separate decorators to handle validation, authorization, and error handling, the
getHomeAddress
function is able to focus solely on the business logic of retrieving home addresses. This separation of concerns results in cleaner, more maintainable code that is easier to understand and modify. The getHomeAddress
function is no longer cluttered with extraneous code, making it much simpler to work with.Definition of Decorator:
- decorators are essentially functions or higher-order functions depending on their usage. It can be used like:
2. decorators are an object-oriented programming (OOP) feature and can only be used on class or class members, not on non-class functions. The following code is not valid:
getMyHomeAddress is just a standalone function. We cannot really decorate this function with the log decorator.
3. That being said, it is still possible to decorate a standalone function using the Lodash utility function
flow
. This allows for the creation of a chain of functions that can be applied to a standalone function, effectively "decorating" it with additional functionality. Here is an example:a lodash flow example that shows how to decorate functions
In the previous example, the
addDecorator
and doubleDecorator
functions added additional behavior to the add
function without modifying the original function. This is similar to how decorators are used in classes, allowing for the addition of functionality to a class or class member without changing the original code.Another example of using decorators is demonstrated in a Lodash flow function that includes a
catchError
decorator and a throwError
function. The catchError
decorator is able to catch errors that are thrown by the original function, allowing for more robust error handling.a lodash flow example that shows how to use decorator to handle errors for functions
If you’re interested in experimenting with this concept, you can check out the provided Replit link: https://replit.com/@MINGWU1/function-decorators-with-lodash-flow?v=1
Additional Benefits with Decorators
Decorators offer additional benefits beyond simply adding functionality to a class or class member. One such benefit is the ability to enable powerful frameworks and patterns, such as Inversion of Control (IoC) and Dependency Injection (DI).
Inversion of Control (IoC) along with Dependency injection:
In the context of software development, “control” refers to manually creating instances of services or dependencies by calling
new ServiceClass()
within the target class. However, this can become problematic when the ServiceClass
requires additional dependencies, which themselves require other dependencies. This can result in the creation of many instances, making the code difficult to manage.Inversion of Control, or IoC, is a pattern that addresses this issue by delegating the responsibility of managing complicated dependency relationships to the framework. Instead of manually creating instances, the framework automatically injects the necessary dependencies into the target class when they are needed.
Dependency Injection, or DI, is a related pattern that involves injecting dependencies into a class rather than having the class create them itself. This helps to reduce coupling between classes and makes the code more modular and easier to maintain.
IoC and DI are heavily used in popular frameworks such as Angular, NestJS, .Net, and Spring Boot, allowing for more efficient and scalable application development.
- Let’s see a very simple angular LoggerService service and an app-root component:
A LoggerService class with an Injectable decorator
An app-root component that depends on the LoggerService class
In Angular, the @Injectable() decorator is used to register metadata for a class that can be injected as a dependency into other classes.
When a class is decorated with @Injectable(), it is recognized by the Angular Injector as a dependency. This means that if another class has a constructor that requires an instance of the @Injectable() class, the Angular Injector will automatically create an instance of the class and pass it to the constructor.
This allows Angular to act as a container that generates instances of services and injects them into the target class, without the need for manual instantiation. This is an example of Inversion of Control (IoC) and Dependency Injection (DI) at work in Angular.
2. Let’s see another simple example in nestjs
A nestjs controller with a Get and Post request handler
The
@Controller
, @Get
, and @Post
decorators are used to add metadata to the UserController
class and its methods. This metadata informs the NestJS framework which routes and HTTP methods should be associated with each method.When a user makes a request to the web application, the framework checks the requested route and HTTP method against the metadata defined in the
UserController
class. If there is a match, the appropriate method is called and the result is returned to the user.For example, if a user makes a GET request to the
/user/list
route, the framework checks the metadata defined by the @Get('/list')
decorator and calls the userList()
method. The userList()
method then executes and returns the appropriate response to the user.Similarly, if a user makes a POST request to the
/user/add
route, the framework checks the metadata defined by the @Post('/add')
decorator and calls the addUser()
method. The addUser()
method then executes and returns the appropriate response to the user.By using decorators to define the routes and HTTP methods for each method in the
UserController
class, we can keep our code clean and easy-to-read. This eliminates the need to manually define each route and method, saving us time and effort.Aspect-oriented programming (AOP)
Do you still remember the very first code example called CustomerHomeAddress class? We used decorators to abstract away parameter validation and error handling in the
CustomerHomeAddress
class. However, issues like these are not limited to a single class or service, and can be present throughout an entire module or application. These issues are known as cross-cutting concerns.The use of TypeScript decorators can be seen as a form of Aspect-Oriented Programming (AOP). By using decorators, you can separate these cross-cutting concerns from the rest of your application code, making them easier to manage and modify. This is similar to how AOP allows you to separate cross-cutting concerns using aspects, which can be applied to multiple parts of your application code.
Online React Examples with Decorators
Now, let’s take a look at some interesting examples in React and explore how decorators can be used to address issues such as validation, cache, and state management. While we won’t go into the specifics of how these solutions are implemented using decorators, you can experiment with them yourself to see how they work.
- Implementing validation with decorators in react
A react validation example with decorators
In the following example, we have a
Course
class that includes a title
and price
property. To ensure that the title
is required and the price
is a positive number, we use decorators to add metadata to the Course
class.A Course class with title and price. The title must be required and the price must be a positive number.
The
Required
and ValueType
functions are used to add this metadata to the Course
class. Additionally, a validator
function reads this metadata and performs the necessary validation logic for the title
and price
properties. By using decorators in this way, we can ensure that our Course
class adheres to the required validation rules without cluttering our code with extraneous logic.2. Implementing cache with decorators in react
A react cache example with decorator
Here is the class with the decorator:
A Student class with cacheable decorator
This code defines a Student class that has a decorator called “cacheable”. When the “fetch address” button is clicked, it will trigger an API call that returns “Melbourne” after 2 seconds, and the result can be cached for 5 seconds. If the button is clicked again within 5 seconds, the cached result will be immediately returned. The “cacheable” decorator is responsible for caching the result for 5 seconds, and it can be used to add metadata to the Student class.
3. Implementing state management with decorators in react
A react state management example with decorators and proxy
Here is the decorated class:
A Store class with a reactive decorator
This code snippet represents a parent component (blue box) and a child component (pink box) that both display the message “Hello World”. Both the parent and child have a button that can modify their own message. It’s worth noting that the parent component did not pass any information to the child component.
Additionally, the code includes a “Store” class that has been decorated with a “reactive” decorator. This decorator makes the “message” property of the “Store” class reactive, meaning that any subscribers who read this value will be notified and updated simultaneously whenever the value changes.
Javascript Decorators
During our previous discussions, we focused extensively on TypeScript decorators. However, it’s important to note that TypeScript decorators are currently in stage 1 of implementation, while JavaScript decorators have progressed to stage 3. As a result, there are some notable differences between the two that we should keep in mind.
- The type of decorator is different:
The type of Javascript stage 3 decorator: tc39/proposal-decorators: Decorators for ES6 classes (github.com)
The type of a class method typescript decorator: A Complete Guide to TypeScript Decorators | Disenchanted (mirone.me)
When comparing JavaScript decorators to TypeScript decorators, it’s clear that there are significant differences between the two.
2. Javascript decorator does not support decorating parameters and metadata.
3. New class auto-accessors
Javascript class accessor
The new accessor in JavaScript shares many similarities with the auto-property feature in C#.
Conclusion:
Through online React examples, we have learned about decorators and how they can be used to solve specific problems. It’s worth noting that while there are significant differences between JavaScript stage 3 decorators and TypeScript decorators, the core functionality of decorators remains the same. As a result, it’s important to be cautious when creating custom decorators, as they may need to be rewritten in the future.