Module 11: Decorators
Decorators provide a way to add annotations and meta-programming to class declarations and members. They're widely used in frameworks like Angular and NestJS.
1. Enabling Decorators
Enable experimental decorator support in tsconfig.json.
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
2. Class Decorators
Modify or replace a class definition.
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
}
Class Decorator with Parameters
function Component(config: { selector: string }) {
return function(constructor: Function) {
console.log(`Component: ${config.selector}`);
console.log(`Class: ${constructor.name}`);
};
}
@Component({ selector: "app-user" })
class UserComponent {
// Component implementation
}
3. Method Decorators
Add behavior to methods.
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Calling ${propertyKey} with args:`, args);
const result = originalMethod.apply(this, args);
console.log(`Result:`, result);
return result;
};
return descriptor;
}
class Calculator {
@log
add(a: number, b: number): number {
return a + b;
}
}
let calc = new Calculator();
calc.add(5, 3);
// Output:
// Calling add with args: [5, 3]
// Result: 8
4. Property Decorators
Add metadata or behavior to properties.
function readonly(target: any, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
writable: false
});
}
class User {
@readonly
id: number = 1;
name: string = "Alice";
}
let user = new User();
// user.id = 2; // Error in strict mode
user.name = "Bob"; // ✅ OK
Validation Decorator
function Min(min: number) {
return function(target: any, propertyKey: string) {
let value: number;
const getter = () => value;
const setter = (newValue: number) => {
if (newValue < min) {
throw new Error(`${propertyKey} must be at least ${min}`);
}
value = newValue;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
};
}
class Product {
@Min(0)
price: number = 0;
}
let product = new Product();
product.price = 100; // ✅ OK
// product.price = -10; // ❌ Error: price must be at least 0
5. Parameter Decorators
Add metadata to method parameters.
function logParameter(target: any, propertyKey: string, parameterIndex: number) {
console.log(`Parameter in ${propertyKey} at index ${parameterIndex}`);
}
class Service {
greet(@logParameter message: string, @logParameter name: string): void {
console.log(`${message}, ${name}`);
}
}
6. Accessor Decorators
Decorate getters and setters.
function configurable(value: boolean) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
};
}
class Point {
private _x: number = 0;
@configurable(false)
get x() {
return this._x;
}
set x(value: number) {
this._x = value;
}
}
7. Decorator Factories
Functions that return decorators.
function authorize(roles: string[]) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Checking authorization for roles: ${roles.join(", ")}`);
// Authorization logic here
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class UserController {
@authorize(["admin"])
deleteUser(id: number): void {
console.log(`Deleting user ${id}`);
}
@authorize(["admin", "moderator"])
updateUser(id: number, data: any): void {
console.log(`Updating user ${id}`);
}
}
8. Multiple Decorators
Apply multiple decorators to a single target.
function first() {
console.log("first(): factory evaluated");
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("first(): called");
};
}
function second() {
console.log("second(): factory evaluated");
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("second(): called");
};
}
class Example {
@first()
@second()
method() {}
}
// Output:
// first(): factory evaluated
// second(): factory evaluated
// second(): called
// first(): called
- Factories are evaluated top to bottom
- Decorators are called bottom to top
9. Metadata Reflection
Use reflect-metadata library for metadata.
npm install reflect-metadata
import "reflect-metadata";
const REQUIRED_KEY = Symbol("required");
function required(target: any, propertyKey: string, parameterIndex: number) {
let existingRequiredParameters: number[] =
Reflect.getOwnMetadata(REQUIRED_KEY, target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata(
REQUIRED_KEY,
existingRequiredParameters,
target,
propertyKey
);
}
function validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const method = descriptor.value;
descriptor.value = function(...args: any[]) {
let requiredParameters: number[] =
Reflect.getOwnMetadata(REQUIRED_KEY, target, propertyKey) || [];
for (let parameterIndex of requiredParameters) {
if (args[parameterIndex] === undefined || args[parameterIndex] === null) {
throw new Error(`Parameter at index ${parameterIndex} is required`);
}
}
return method.apply(this, args);
};
}
class UserService {
@validate
createUser(@required name: string, age: number): void {
console.log(`Creating user: ${name}, ${age}`);
}
}
let service = new UserService();
service.createUser("Alice", 25); // ✅ OK
// service.createUser(null, 25); // ❌ Error: Parameter required
10. Real-World Example: HTTP Controller
function Controller(prefix: string) {
return function(target: Function) {
Reflect.defineMetadata("prefix", prefix, target);
};
}
function Get(path: string) {
return function(target: any, propertyKey: string) {
Reflect.defineMetadata("path", path, target, propertyKey);
Reflect.defineMetadata("method", "GET", target, propertyKey);
};
}
function Post(path: string) {
return function(target: any, propertyKey: string) {
Reflect.defineMetadata("path", path, target, propertyKey);
Reflect.defineMetadata("method", "POST", target, propertyKey);
};
}
@Controller("/users")
class UserController {
@Get("/")
getAll(): string[] {
return ["Alice", "Bob"];
}
@Get("/:id")
getById(id: number): string {
return `User ${id}`;
}
@Post("/")
create(userData: any): string {
return "User created";
}
}
11. Caching Decorator
function Memoize(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
const cache = new Map();
descriptor.value = function(...args: any[]) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log("Returning cached result");
return cache.get(key);
}
const result = originalMethod.apply(this, args);
cache.set(key, result);
return result;
};
return descriptor;
}
class MathService {
@Memoize
fibonacci(n: number): number {
console.log(`Computing fibonacci(${n})`);
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}
let math = new MathService();
console.log(math.fibonacci(10)); // Computes and caches
console.log(math.fibonacci(10)); // Returns cached
12. Timing Decorator
function Timing(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function(...args: any[]) {
const start = performance.now();
const result = await originalMethod.apply(this, args);
const end = performance.now();
console.log(`${propertyKey} took ${(end - start).toFixed(2)}ms`);
return result;
};
return descriptor;
}
class DataService {
@Timing
async fetchData(): Promise<string[]> {
await new Promise(resolve => setTimeout(resolve, 1000));
return ["data1", "data2"];
}
}
Key Takeaways
✅ Class decorators modify class constructors
✅ Method decorators wrap method behavior
✅ Property decorators add metadata to properties
✅ Parameter decorators annotate parameters
✅ Decorator factories create configurable decorators
✅ Execution order: factories top-down, decorators bottom-up
✅ Common in Angular, NestJS, TypeORM
Practice Exercises
Exercise 1: Deprecated Decorator
function Deprecated(message: string) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.warn(`⚠️ ${propertyKey} is deprecated: ${message}`);
return originalMethod.apply(this, args);
};
};
}
class OldAPI {
@Deprecated("Use newMethod() instead")
oldMethod(): void {
console.log("Old implementation");
}
}
Exercise 2: Rate Limit Decorator
Create a decorator that limits method calls per time period.
Exercise 3: Validation Decorator
Build property validation decorators (Required, Email, Min, Max).
Next Steps
In Module 12, we'll explore Declaration Files (.d.ts) for typing third-party JavaScript libraries.