Explore TypeScript's built-in support for decorators and learn how to implement the Decorator pattern using TypeScript's syntax and type features.
In this section, we will delve into the implementation of the Decorator pattern in TypeScript. As expert developers, you’re likely familiar with the concept of decorators in programming. TypeScript, with its robust type system and syntactical sugar, offers a powerful way to implement decorators, allowing us to add functionality to classes and their members while preserving type safety.
TypeScript provides built-in support for decorators, which are special kinds of declarations that can be attached to classes, methods, accessors, properties, or parameters. Decorators are a stage 2 proposal for JavaScript and are available as an experimental feature in TypeScript. To enable decorators, you must set the experimentalDecorators
compiler option to true
in your tsconfig.json
.
{
"compilerOptions": {
"experimentalDecorators": true
}
}
Let’s explore how to create custom decorators in TypeScript. We’ll start with a simple method decorator that logs the execution time of a method.
function LogExecutionTime(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Method ${propertyKey} execution started`);
const start = performance.now();
const result = originalMethod.apply(this, args);
const end = performance.now();
console.log(`Method ${propertyKey} execution finished in ${end - start}ms`);
return result;
};
return descriptor;
}
class ExampleService {
@LogExecutionTime
fetchData() {
// Simulate a network request
for (let i = 0; i < 1e6; i++) {}
return "Data fetched";
}
}
const service = new ExampleService();
service.fetchData();
In this example, the LogExecutionTime
decorator is applied to the fetchData
method. This decorator logs the start and end time of the method execution, providing insights into performance.
TypeScript’s type system allows us to create decorators that are both powerful and type-safe. Let’s create a property decorator that validates if a property is a non-empty string.
function NonEmptyString(target: Object, propertyKey: string) {
let value: string;
const getter = function () {
return value;
};
const setter = function (newVal: string) {
if (!newVal || newVal.trim().length === 0) {
throw new Error(`Property ${propertyKey} cannot be empty`);
}
value = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
class User {
@NonEmptyString
name: string;
constructor(name: string) {
this.name = name;
}
}
const user = new User("John Doe");
console.log(user.name); // John Doe
// user.name = ""; // Throws error: Property name cannot be empty
This example demonstrates a NonEmptyString
decorator that ensures a property is never set to an empty string. If an empty string is assigned, an error is thrown.
Decorators can also be used to modify or augment classes. Let’s create a class decorator that adds a static method to a class.
function AddStaticMethod(target: Function) {
target.prototype.newStaticMethod = function () {
console.log("New static method added!");
};
}
@AddStaticMethod
class MyClass {}
const myInstance = new MyClass();
(myInstance as any).newStaticMethod(); // New static method added!
In this example, the AddStaticMethod
decorator adds a new static method to MyClass
. This demonstrates how decorators can be used to enhance class functionality dynamically.
Experiment with the provided code examples by modifying them. For instance, try creating a decorator that logs method arguments or a class decorator that tracks the number of instances created.
To better understand how decorators interact with classes and methods, let’s visualize the flow using a diagram.
classDiagram class ExampleService { +fetchData() : string } class LogExecutionTime { +apply(target, propertyKey, descriptor) } ExampleService --> LogExecutionTime : @LogExecutionTime
Diagram Description: This class diagram illustrates the relationship between the ExampleService
class and the LogExecutionTime
decorator. The decorator is applied to the fetchData
method, enhancing its functionality.
Remember, this is just the beginning. As you progress, you’ll discover more ways to leverage decorators for enhancing your TypeScript applications. Keep experimenting, stay curious, and enjoy the journey!
In this section, we explored how to implement the Decorator pattern in TypeScript using its built-in support for decorators. We covered various types of decorators, provided code examples, and discussed best practices. By leveraging TypeScript’s type system, we can create decorators that are both powerful and type-safe.