Learn TypeScript With Me in 14 Days Day 11
Day 11: Decorators in TypeScript
In this part of the series, we will be learning one of the more advanced and powerful features in TypeScript: Decorators. If you are following this series, the last few parts may have been a bit advanced if you are new to TypeScript and may require more research and reading to fully grasp the concepts. This is why I have made sure to add a section for useful resources at the end of each part.
Decorators provide a way to modify or enhance the behaviour of classes, methods, and properties without changing their implementation directly. By the end of this post, will have learnt how to create and apply decorators and see how they can simplify certain patterns in our code, especially for features like logging, validation, and metadata handling.
What Are Decorators?
Decorators are a special type of declaration that can be attached to a class, method, property, or parameter. They allow us to modify the behaviour of the decorated entity. Decorators are widely used in frameworks like Angular for dependency injection and NestJS for building server-side applications, but we can also use them to enhance our own applications even if we are not using those frameworks.
In TypeScript, decorators are implemented using ES7 decorator syntax and are essentially functions that take specific arguments depending on what they are decorating.
Enabling Experimental Decorators
Before we dive into decorators, make sure you enable decorators in TypeScript by adding the following setting to your tsconfig.json
file:
{
"compilerOptions": {
"experimentalDecorators": true
}
}
This allows TypeScript to recognise the decorator syntax.
Understanding Class Decorators
A Class Decorator is a function that is applied to the class constructor. It can be used to modify or add behaviour to a class without changing its definition.
Example of a Simple Class Decorator:
function logClass(constructor: Function) {
console.log(`Class "${constructor.name}" is being created.`);
}
@logClass
class Task {
constructor(public title: string) {}
}
const task = new Task('Learn TypeScript');
Explanation:
When the Task class is defined with the @logClass decorator, the logClass function is called with the Task class constructor as its argument. This results in the following output to the console;
Class "Task" is being created.
- @logClass: The logClass function is a decorator that logs a message when the
Task
class is instantiated. - Class Decorators: They receive the class constructor as an argument, allowing you to modify or extend the behaviour of the class.
Method Decorators
A Method Decorator is applied to a method within a class and allows you to modify or enhance the behaviour of that method.
Example of a Method Decorator for Logging Execution Time:
function logExecutionTime(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.time(`${propertyKey} execution time`);
const result = originalMethod.apply(this, args);
console.timeEnd(`${propertyKey} execution time`);
return result;
};
}
class TaskManager {
@logExecutionTime
completeTask(taskId: number) {
for (let i = 0; i < 1e6; i++) {} // Simulating a time-consuming operation
console.log(`Task ${taskId} completed.`);
}
}
const taskManager = new TaskManager();
taskManager.completeTask(1);
Explanation:
- The logExecutionTime function is a decorator that logs the execution time of a method.
- The @logExecutionTime decorator is applied to the completeTask method in the TaskManager class, causing the logExecutionTime function to be called with the completeTask method’s descriptor.
- The completeTask method simulates a time-consuming operation and logs a message.
- Creating an instance of the TaskManager class and calling the completeTask method results in the execution time being logged to the console.
When the TaskManager class is defined with the @logExecutionTime decorator applied to the completeTask method, the logExecutionTime function is called with the completeTask method’s descriptor. This results in the completeTask method being overridden to include timing logic.
When the completeTask method is called on the taskManager instance, the following happens: 1 The timer starts with the label “completeTask execution time”. 2 The original completeTask method is executed, simulating a time-consuming operation and logging a message. 3 The timer ends with the label “completeTask execution time”. 4 The result of the original method is returned.
Property Decorators
A Property Decorator is applied to a class property and can be used to modify or add metadata to properties.
Example of a Property Decorator for Validation:
function minLength(length: number) {
return function (target: any, propertyKey: string) {
let value: string;
const getter = function () {
return value;
};
const setter = function (newVal: string) {
if (newVal.length < length) {
throw new Error(`The ${propertyKey} must be at least ${length} characters long.`);
}
value = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
});
};
}
class Task {
@minLength(5)
title: string;
constructor(title: string) {
this.title = title;
}
}
try {
const task = new Task('Todo');
} catch (error) {
console.error(error.message); // Output: The title must be at least 5 characters long.
}
Explanation:
When the Task class is defined with the @minLength(5) decorator applied to the title property, the minLength function is called with the title property’s descriptor. This results in the title property being overridden to include validation logic.
When a new instance of the Task class is created with the title ‘Todo’, the following happens:
- The title property’s setter function is called with the value ‘Todo’.
- The setter function checks if the length of ‘Todo’ is at least 5 characters.
- Since ‘Todo’ is only 4 characters long, an error is thrown with the message “The title must be at least 5 characters long.”.
- The error is caught in the catch block, and the error message is logged to the console.
- Use Case: Property decorators can be used for validation, logging, or enforcing constraints on class properties.
Parameter Decorators
A Parameter Decorator is used to add metadata to the parameters of class methods. These are less commonly used but can be helpful in certain scenarios like dependency injection or logging parameter usage.
Example of a Parameter Decorator for Logging Parameter Usage:
function logParameter(target: any, propertyKey: string, parameterIndex: number) {
const originalMethod = target[propertyKey];
target[propertyKey] = function (...args: any[]) {
console.log(`Parameter at index ${parameterIndex} for method ${propertyKey}:`, args[parameterIndex]);
return originalMethod.apply(this, args);
};
}
class TaskManager {
completeTask(@logParameter taskId: number) {
console.log(`Completing task ${taskId}`);
}
}
const taskManager = new TaskManager();
taskManager.completeTask(42);
Explanation:
When the TaskManager class is defined with the @logParameter decorator applied to the taskId parameter of the completeTask method, the logParameter function is called with the completeTask method’s descriptor and the index of the taskId parameter. This results in the completeTask method being overridden to include logging logic.
When the completeTask method is called on the taskManager instance with the argument 42, the following happens: 1 The overridden completeTask method is executed. 2 The value of the parameter at the specified index (in this case, taskId at index 0) is logged to the console. 3 The original completeTask method is called with the same arguments, and its result is returned.
- Use Case: Parameter decorators are useful for logging, validation, or dependency injection on specific method parameters.
Applying Decorators to the Task Manager
Let’s attempt to apply some decorators to the Task Manager project to improve logging, validation, and behaviour.
1. Logging Class Creation with a Class Decorator:
function logClassCreation(constructor: Function) {
console.log(`TaskManager class is being created.`);
}
@logClassCreation
class TaskManager {
completeTask(taskId: number) {
console.log(`Completing task ${taskId}`);
}
}
2. Validating Task Title Length with a Property Decorator:
function minLength(length: number) {
return function (target: any, propertyKey: string) {
let value: string;
const getter = function () {
return value;
};
const setter = function (newVal: string) {
if (newVal.length < length) {
throw new Error(`The ${propertyKey} must be at least ${length} characters long.`);
}
value = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
});
};
}
class Task {
@minLength(3)
title: string;
constructor(title: string) {
this.title = title;
}
}
3. Logging Execution Time for Completing Tasks with a Method Decorator:
function logExecutionTime(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.time(`${propertyKey} execution time`);
const result = originalMethod.apply(this, args);
console.timeEnd(`${propertyKey} execution time`);
return result;
};
}
class TaskManager {
@logExecutionTime
completeTask(taskId: number) {
console.log(`Task ${taskId} completed.`);
}
}
What’s Next?
In this part of the series, we have learnt how to use decorators in TypeScript to modify and enhance the behaviour of classes, methods, properties, and parameters. Decorators provide a clean and reusable way to inject additional functionality like logging, validation, and metadata handling without modifying the core logic of our application.
Tomorrow, we’ll dive into TypeScript with VueJS to see how TypeScript can improve the development experience for modern web applications.