Mastering TypeScript Decorators: Practical Use Cases and Advanced Patterns
Learn how to use TypeScript decorators with practical examples and advanced patterns to enhance your code with clean, reusable, and powerful annotations.
TypeScript decorators provide a way to add annotations and meta-programming syntax for class declarations and members. They help improve code readability and reusability by allowing you to modify classes, methods, properties, or parameters in a declarative way. In this tutorial, we will cover the basics of decorators, practical use cases, and some advanced patterns to master this powerful feature of TypeScript.
### What Are Decorators? A Quick Overview Decorators are special functions that can be attached to classes, methods, properties, or method parameters. They receive metadata about the target they decorate and optionally modify or extend its behavior. To enable decorators in your TypeScript project, ensure you have "experimentalDecorators" set to true in your tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true
}
}### Creating Your First Simple Class Decorator Let's start with a simple example: a class decorator that logs when an instance of the class is created.
function LogCreation(target: Function) {
const original = target;
function construct(constructor: any, args: any[]) {
console.log(`Creating instance of: ${constructor.name}`);
return new constructor(...args);
}
const f: any = function(...args: any[]) {
return construct(original, args);
};
f.prototype = original.prototype;
return f;
}
@LogCreation
class Person {
constructor(public name: string) {}
}
const p = new Person('Alice');
// Console output: Creating instance of: Person### Method Decorators: Adding Execution Time Logging Method decorators can enhance existing methods. Here's an example that logs how long a method takes to execute.
function LogExecutionTime(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.time(propertyKey);
const result = originalMethod.apply(this, args);
console.timeEnd(propertyKey);
return result;
};
return descriptor;
}
class MathOps {
@LogExecutionTime
factorial(n: number): number {
if (n <= 1) return 1;
return n * this.factorial(n - 1);
}
}
const math = new MathOps();
math.factorial(5);
// Console output will show timed logs for factorial method### Property Decorators: Validating Values You can use property decorators to enforce validation rules on class properties. Below is a simple decorator that ensures a string property is never empty.
function NonEmpty(target: any, propertyKey: string) {
let value = target[propertyKey];
const getter = () => value;
const setter = (newVal: string) => {
if (!newVal || newVal.trim().length === 0) {
throw new Error(`${propertyKey} cannot be empty.`);
}
value = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
class User {
@NonEmpty
name: string = '';
constructor(name: string) {
this.name = name;
}
}
const user = new User('Bob');
// user.name = ''; // Throws error: name cannot be empty.### Parameter Decorators: Logging Metadata Parameter decorators allow you to observe or modify parameters of methods. Here’s an example that logs parameter indexes.
function LogParameter(target: any, propertyKey: string, parameterIndex: number) {
console.log(
`Parameter at position ${parameterIndex} in method ${propertyKey} has been decorated.`
);
}
class Greeter {
greet(@LogParameter message: string): void {
console.log(message);
}
}
const greeter = new Greeter();
greeter.greet('Hello!');
// Console output: Parameter at position 0 in method greet has been decorated.### Advanced Pattern: Creating a Reusable Authentication Decorator As an advanced example, consider a method decorator that only allows method execution if a user is authenticated.
function Authenticated(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
if (!this.isAuthenticated) {
throw new Error('User is not authenticated.');
}
return originalMethod.apply(this, args);
};
return descriptor;
}
class Service {
isAuthenticated = false;
@Authenticated
fetchData() {
return 'Sensitive data';
}
}
const service = new Service();
// service.fetchData(); // Throws error: User is not authenticated.
service.isAuthenticated = true;
console.log(service.fetchData()); // Outputs: Sensitive data### Summary TypeScript decorators are a powerful tool that can cleanly add functionality to your code — from simple logging and validation to complex behaviors like authentication or caching. Mastering them involves understanding decorators on classes, methods, properties, and parameters, along with practical use cases and more advanced decorator factories and patterns. Experiment with these examples to make your TypeScript code more modular and expressive!