Skip to main content

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
Decorator Execution Order
  • 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.