Decorators in JavaScript and TypeScript provide a powerful way to modify or extend the behavior of classes, methods, properties, and parameters. They simplify repetitive tasks such as logging, validation, and caching, and are widely used in frameworks like Angular, NestJS, and TypeORM.
In this guide, we’ll cover:
- What are decorators, and why should you use them?
- Types of decorators with practical examples.
- How to implement advanced use cases with
reflect-metadata
. - Common pitfalls and best practices.
What Are Decorators?
A decorator is a function that adds behavior to a class, method, property, or parameter. Think of it as a wrapper that enhances functionality without altering the original implementation.
Why Use Decorators?
- Code Reusability: Abstract repetitive tasks into reusable decorators.
- Readability: Use expressive syntax to define cross-cutting concerns (e.g., logging, validation).
- Framework Compatibility: Decorators are integral to frameworks like NestJS, Angular, and TypeORM.
- Maintainability: Keep business logic clean by separating additional behaviors.
Types of Decorators with Examples
1. Function Decorators
Function decorators wrap standalone functions to modify their behavior.
Example: Adding Execution Time Logging
function withExecutionTime(fn: Function) {
return function (...args: any[]) {
console.time('Execution Time');
const result = fn(...args);
console.timeEnd('Execution Time');
return result;
};
}
// Original function
function calculateSum(a: number, b: number) {
return a + b;
}
// Wrap function with the decorator
const calculateSumWithTiming = withExecutionTime(calculateSum);
console.log(calculateSumWithTiming(5, 10));
// Output:
// Execution Time: 0.123ms
// 15
2. Class Decorators
Class decorators modify or enhance an entire class.
Example: Tracking Instances
function TrackInstances(constructor: Function) {
let count = 0;
return class extends constructor {
constructor(...args: any[]) {
super(...args);
count++;
console.log(`Instance count: ${count}`);
}
};
}
@TrackInstances
class User {
constructor(public name: string) {}
}
new User('Alice'); // Instance count: 1
new User('Bob'); // Instance count: 2
3. Method Decorators
Method decorators add functionality to specific methods.
Example: Logging Method Calls
function LogMethod(target: any, methodName: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Method ${methodName} called with arguments:`, args);
return originalMethod.apply(this, args);
};
}
class Calculator {
@LogMethod
add(a: number, b: number) {
return a + b;
}
}
const calc = new Calculator();
console.log(calc.add(10, 5));
// Output:
// Method add called with arguments: [10, 5]
// 15
4. Property Decorators
Property decorators attach metadata or behaviors to class properties.
Example: Marking Required Properties
import 'reflect-metadata';
function Required(target: any, propertyKey: string) {
Reflect.defineMetadata('required', true, target, propertyKey);
}
function Validate(instance: any) {
for (const key in instance) {
if (Reflect.getMetadata('required', instance, key) && !instance[key]) {
throw new Error(`${key} is required`);
}
}
}
class User {
@Required
name: string;
constructor(name: string) {
this.name = name;
Validate(this);
}
}
new User('Alice'); // Works fine
new User(''); // Throws: name is required
5. Parameter Decorators
Parameter decorators add metadata to method parameters.
Example: Validating Parameters
import 'reflect-metadata';
function RequiredParam(target: any, methodName: string, paramIndex: number) {
const existingParams = Reflect.getMetadata('requiredParams', target, methodName) || [];
existingParams.push(paramIndex);
Reflect.defineMetadata('requiredParams', existingParams, target, methodName);
}
function ValidateParams(target: any, methodName: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const requiredParams = Reflect.getMetadata('requiredParams', target, methodName) || [];
requiredParams.forEach((index: number) => {
if (args[index] === undefined) {
throw new Error(`Missing required parameter at index ${index}`);
}
});
return originalMethod.apply(this, args);
};
}
class MathOperations {
@ValidateParams
multiply(@RequiredParam a: number, b: number) {
return a * b;
}
}
const math = new MathOperations();
console.log(math.multiply(10, 5)); // Works
console.log(math.multiply(undefined, 5)); // Throws: Missing required parameter at index 0
6. Accessor Decorators
Accessor decorators modify getters and setters.
Example: Caching Getter Results
function Cache(target: any, key: string, descriptor: PropertyDescriptor) {
const originalGetter = descriptor.get;
const cacheKey = `__${key}_cache`;
descriptor.get = function () {
if (!this[cacheKey]) {
this[cacheKey] = originalGetter?.apply(this);
}
return this[cacheKey];
};
}
class ExpensiveComputation {
private counter = 0;
@Cache
get compute() {
console.log('Computing...');
return ++this.counter;
}
}
const obj = new ExpensiveComputation();
console.log(obj.compute); // Computing... 1
console.log(obj.compute); // 1 (cached)
Framework Use Cases
1. NestJS
@Controller
,@Get
,@Post
: Define routes.@Injectable
,@Module
: Manage dependencies.
2. TypeORM
@Entity
,@Column
,@PrimaryGeneratedColumn
: Define database models.
3. Angular
@Component
,@Directive
: Define components and directives.
Best Practices for Using Decorators
- Avoid Overuse: Use decorators judiciously to avoid clutter.
- Follow Standards: Use
reflect-metadata
for advanced use cases. - Document Well: Provide clear documentation for custom decorators.
- Test Thoroughly: Ensure decorators behave as expected in edge cases.
Conclusion
Decorators are an essential tool in TypeScript and JavaScript, simplifying complex behaviors and enhancing code readability. Whether you’re building APIs with NestJS, managing databases with TypeORM, or creating UI components in Angular, understanding decorators unlocks powerful possibilities in modern development.
Explore robust solutions with Cybermindworks. Contact us to bring your ideas to life!
Further Reading
About Rishaba Priyan
Rishaba Priyan: Frontend Developer | Crafting Seamless User Experiences
At CyberMind Works, Rishaba Priyan excels as a Frontend Developer, specializing in creating intuitive and engaging user interfaces. Leveraging his expertise in technologies like Next.js, Rishaba focuses on delivering seamless digital experiences that blend aesthetics with functionality.