Post

12. Decorators and Metadata

🎨 Master TypeScript decorators and metadata reflection. Learn class, method, property, and parameter decorators with reflect-metadata for building powerful frameworks and libraries. πŸš€

12. Decorators and Metadata

What we will learn in this post?

  • πŸ‘‰ Introduction to Decorators
  • πŸ‘‰ Class Decorators
  • πŸ‘‰ Method Decorators
  • πŸ‘‰ Property and Accessor Decorators
  • πŸ‘‰ Parameter Decorators
  • πŸ‘‰ Reflect Metadata API
  • πŸ‘‰ Decorator Composition and Factories

Introduction to TypeScript Decorators πŸŽ‰

TypeScript decorators are a powerful feature that allows you to add annotations and metadata to your classes, methods, properties, and parameters. They help you enhance your code in a clean and organized way! They’re the foundation of frameworks like Angular and NestJS.

What is a Decorator? πŸ€”

A decorator is a special kind of function that can modify the behavior of a class or its members. You use the @ syntax to apply a decorator.

1
2
3
function MyDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log(`Decorating ${propertyKey}`);
}

Getting Started πŸš€

To use decorators, you need to enable experimentalDecorators in your tsconfig.json:

1
2
3
4
5
6
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Decorator Evaluation Order πŸ”„

Decorators are evaluated from bottom to top (closest to the declaration first), but executed from top to bottom.

1
2
3
4
5
@first()
@second()
class Example {}
// Evaluates: second() then first()
// Executes: first's result, then second's result

Understanding Class Decorators 🎯

Class decorators are functions that take a class constructor as an argument and can modify or replace it. They’re executed when the class is declared.

Creating a Simple Class Decorator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}

@Sealed
class BugReport {
    type = "report";
    title: string;
    
    constructor(t: string) {
        this.title = t;
    }
}

Decorator Factory Patterns 🏭

Decorator factories allow parameterized decorators:

1
2
3
4
5
6
7
8
9
function Component(options: { selector: string; template: string }) {
    return function(constructor: Function) {
        constructor.prototype.selector = options.selector;
        constructor.prototype.template = options.template;
    };
}

@Component({ selector: 'app-root', template: '<h1>Hello</h1>' })
class AppComponent {}

Class Decorator Flow

flowchart TD
    A["🎯 Class Decorator"]:::style1 --> B["πŸ”§ Modify Constructor"]:::style2
    A --> C["βž• Add Properties/Methods"]:::style3
    B --> D["πŸ“ Logging"]:::style4
    B --> E["βœ… Validation"]:::style5
    B --> F["πŸ’‰ Dependency Injection"]:::style4

    classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style4 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style5 fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;

    linkStyle default stroke:#e67e22,stroke-width:3px;

Method Decorators βš™οΈ

Method decorators intercept method calls and can modify behavior, log calls, or add caching.

Creating Method Decorators

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    
    descriptor.value = function(...args: any[]) {
        console.log(`Calling ${propertyKey} with:`, 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;
    }
}

Method Decorator Flow

flowchart TD
    A["πŸš€ Start"]:::style1 --> B{"πŸ“ž Method Call"}:::style2
    B -->|Intercept| C["πŸ” Log/Cache/Validate"]:::style3
    C --> D["⚑ Execute Original Method"]:::style4
    D --> E["πŸ“Š Return Result"]:::style5
    E --> F["βœ… End"]:::style1

    classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style4 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style5 fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;

    linkStyle default stroke:#e67e22,stroke-width:3px;

Property and Accessor Decorators πŸ”§

Property decorators add metadata to properties, while accessor decorators modify get/set behavior.

Property Decorators

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Required(target: any, propertyKey: string) {
    let value: any;
    
    const getter = () => value;
    const setter = (newVal: any) => {
        if (!newVal) {
            throw new Error(`${propertyKey} is required`);
        }
        value = newVal;
    };
    
    Object.defineProperty(target, propertyKey, {
        get: getter,
        set: setter,
        enumerable: true,
        configurable: true
    });
}

class User {
    @Required
    name!: string;
}

Accessor Decorators

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function Validate(min: number, max: number) {
    return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalSet = descriptor.set;
        
        descriptor.set = function(value: number) {
            if (value < min || value > max) {
                throw new Error(`${propertyKey} must be between ${min} and ${max}`);
            }
            originalSet?.call(this, value);
        };
    };
}

class Product {
    private _price: number = 0;
    
    @Validate(0, 10000)
    set price(value: number) {
        this._price = value;
    }
    
    get price(): number {
        return this._price;
    }
}

Parameter Decorators πŸ“‹

Parameter decorators attach metadata to function parameters, commonly used in dependency injection.

Creating Parameter Decorators

1
2
3
4
5
6
7
8
9
10
function Inject(token: string) {
    return function(target: any, propertyKey: string, parameterIndex: number) {
        const existingParams = Reflect.getMetadata('design:paramtypes', target, propertyKey) || [];
        console.log(`Parameter ${parameterIndex} in ${propertyKey} needs ${token}`);
    };
}

class UserService {
    constructor(@Inject('Database') db: any) {}
}

Parameter Decorator Flow

flowchart TD
    A["⚑ Function"]:::style1 --> B["🎯 Parameter Decorator"]:::style2
    B --> C{"πŸ“¦ Metadata"}:::style3
    C --> D["πŸ’‰ Dependency Injection"]:::style4
    C --> E["βœ… Validation"]:::style5

    classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style4 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style5 fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;

    linkStyle default stroke:#e67e22,stroke-width:3px;

Reflect Metadata API πŸ”

The reflect-metadata library enables storing and retrieving design-time type information at runtime.

Setup

1
npm install reflect-metadata
1
import 'reflect-metadata';

Key Metadata Types

  • design:type: Property type
  • design:paramtypes: Parameter types array
  • design:returntype: Return type
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Point {
    x: number;
    y: number;
}

class Line {
    private _p0: Point;
    
    @validate
    set p0(value: Point) {
        this._p0 = value;
    }
}

function validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    let set = descriptor.set!;
    descriptor.set = function(value: any) {
        let type = Reflect.getMetadata("design:type", target, propertyKey);
        if (!(value instanceof type)) {
            throw new TypeError(`Invalid type, expected ${type.name}`);
        }
        set.call(this, value);
    };
}

Metadata Flow

graph TD
    A["🎯 Class"]:::style1 --> B["🎨 Decorator"]:::style2
    B --> C["πŸ“¦ Metadata"]:::style3
    C --> D["πŸ“Š Type Information"]:::style4

    classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style4 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;

    linkStyle default stroke:#e67e22,stroke-width:3px;

Decorator Factories and Composition πŸ—οΈ

Decorator factories return decorators, enabling parameterization and reusability.

Decorator Factory Pattern

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Timeout(milliseconds: number) {
    return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;
        
        descriptor.value = async function(...args: any[]) {
            const timeout = new Promise((_, reject) => 
                setTimeout(() => reject(new Error('Timeout!')), milliseconds)
            );
            const result = Promise.resolve(originalMethod.apply(this, args));
            return Promise.race([result, timeout]);
        };
    };
}

class APIService {
    @Timeout(5000)
    async fetchData(url: string) {
        const response = await fetch(url);
        return response.json();
    }
}

Composing Multiple Decorators

1
2
3
4
5
6
7
8
class UserService {
    @Log()
    @Cache(60000)
    @Timeout(5000)
    async getUser(id: string): Promise<User> {
        // Implementation
    }
}

Real-World Examples 🌍

Example 1: API Rate Limiting Decorator 🚦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function RateLimit(maxCalls: number, windowMs: number) {
    const calls: number[] = [];
    
    return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;
        
        descriptor.value = async function(...args: any[]) {
            const now = Date.now();
            const recentCalls = calls.filter(time => now - time < windowMs);
            
            if (recentCalls.length >= maxCalls) {
                throw new Error(`Rate limit exceeded: ${maxCalls} calls per ${windowMs}ms`);
            }
            
            calls.push(now);
            return originalMethod.apply(this, args);
        };
    };
}

class PaymentService {
    @RateLimit(10, 60000) // 10 calls per minute
    async processPayment(amount: number, cardToken: string) {
        console.log(`Processing payment of $${amount}`);
        return { success: true, transactionId: Math.random().toString(36) };
    }
}

Why This Matters: Protects APIs from abuse and ensures fair resource usage.

Example 2: DTO Validation Decorator πŸ“‹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function ValidateDTO(validationSchema: any) {
    return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;
        
        descriptor.value = function(...args: any[]) {
            const [dto] = args;
            
            for (const [key, rules] of Object.entries(validationSchema)) {
                const value = dto[key];
                const ruleSet = rules as any;
                
                if (ruleSet.required && !value) {
                    throw new Error(`${key} is required`);
                }
                if (ruleSet.minLength && value.length < ruleSet.minLength) {
                    throw new Error(`${key} must be at least ${ruleSet.minLength} characters`);
                }
                if (ruleSet.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
                    throw new Error(`${key} must be a valid email`);
                }
            }
            
            return originalMethod.apply(this, args);
        };
    };
}

const userSchema = {
    email: { required: true, email: true },
    password: { required: true, minLength: 8 }
};

class UserController {
    @ValidateDTO(userSchema)
    createUser(userData: any) {
        return { id: 123, ...userData };
    }
}

Why This Matters: Ensures data integrity before processing, reducing bugs and vulnerabilities.

Example 3: Performance Monitoring Decorator ⏱️

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function Measure(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    
    descriptor.value = async function(...args: any[]) {
        const start = performance.now();
        
        try {
            const result = await originalMethod.apply(this, args);
            const duration = performance.now() - start;
            
            console.log(`⏱️ ${propertyKey} took ${duration.toFixed(2)}ms`);
            
            if (duration > 1000) {
                console.warn(`⚠️ Slow method: ${propertyKey}`);
            }
            
            return result;
        } catch (error) {
            const duration = performance.now() - start;
            console.error(`❌ ${propertyKey} failed after ${duration.toFixed(2)}ms`);
            throw error;
        }
    };
    
    return descriptor;
}

class DatabaseService {
    @Measure
    async fetchUsers(): Promise<any[]> {
        await new Promise(resolve => setTimeout(resolve, 150));
        return [{ id: 1, name: 'Alice' }];
    }
}

Why This Matters: Automatically tracks performance without manual instrumentation.

🧠 Test Your Knowledge

What is the correct syntax to enable decorators in tsconfig.json?
Explanation

You must add "experimentalDecorators": true inside the "compilerOptions" object in tsconfig.json to enable decorator support.

In which order are multiple decorators evaluated on a method?
Explanation

Decorators are evaluated bottom-to-top (closest to the method first), meaning @A @B method() evaluates B first, then A wraps B's result.

What does the PropertyDescriptor parameter provide in a method decorator?
Explanation

PropertyDescriptor gives you access to the method's value (the function itself), and metadata like writable, enumerable, and configurable, allowing you to modify behavior.

What is the purpose of the reflect-metadata library?
Explanation

reflect-metadata allows you to attach and retrieve metadata to/from classes, methods, properties, and parameters at runtime, enabling features like dependency injection.

Which TypeScript option must be enabled to emit type metadata for decorators?
Explanation

emitDecoratorMetadata: true in tsconfig.json tells TypeScript to automatically emit design-time type information (design:type, design:paramtypes, design:returntype) for decorated elements.


🎯 Hands-On Assignment: Build a Retry Decorator with Exponential Backoff πŸš€

πŸ“ Your Mission

Create a decorator factory that automatically retries failed async operations with exponential backoff, commonly used in network requests and external API integrations.

🎯 Requirements

  1. Create a @Retry decorator factory that accepts:
    • maxAttempts: Maximum number of retry attempts
    • delayMs: Initial delay in milliseconds
    • backoffMultiplier: Multiplier for exponential backoff (default 2)
  2. On failure, wait delayMs * (backoffMultiplier ^ attempt) before retrying
  3. Log each retry attempt with attempt number and delay
  4. If all attempts fail, throw the last error
  5. If successful before max attempts, return immediately

πŸ’‘ Implementation Hints

  1. Use a for loop to track attempts
  2. Wrap method call in try-catch to detect failures
  3. Use await new Promise(resolve => setTimeout(resolve, delay)) for delays
  4. Calculate delay: delayMs * Math.pow(backoffMultiplier, attempt)
  5. Return immediately on success, continue loop on error

πŸš€ Example Input/Output

style="background: #2c3e50; color: #ecf0f1; padding: 20px; border-radius: 8px; overflow-x: auto; margin: 15px 0;">class ExternalAPI { private callCount = 0; @Retry(3, 1000, 2) // 3 attempts, 1s initial delay, 2x backoff async unreliableCall(): Promise { this.callCount++; if (this.callCount < 3) { throw new Error(`Attempt ${this.callCount} failed`); } return 'Success!'; } } const api = new ExternalAPI(); const result = await api.unreliableCall(); // Console output: // ❌ Attempt 1 failed, retrying in 1000ms... // ❌ Attempt 2 failed, retrying in 2000ms... // βœ… Success! // result = 'Success!' </code></pre>

πŸ† Bonus Challenges

  • Level 2: Add a retryOnErrors parameter to only retry specific error types
  • Level 3: Implement jitter (random delay variation) to prevent thundering herd
  • Level 4: Add metrics tracking (total retries, success rate, average latency)
  • Level 5: Create a circuit breaker pattern that stops retrying after consecutive failures

πŸ“š Learning Goals

  • Master decorator factory patterns with parameters 🎯
  • Handle async operations in decorators ✨
  • Implement exponential backoff algorithms πŸ”„
  • Practice error handling and retry logic πŸ”—
  • Build production-ready resilience patterns πŸ› οΈ

πŸ’‘ Pro Tip: This pattern is used in AWS SDK, Axios retry, and Polly.js for resilient network calls!

Share Your Solution! πŸ’¬

Completed the project? Post your code in the comments below! Show us your TypeScript decorator mastery! πŸš€βœ¨

</div> </details> --- # Conclusion πŸŽ“ TypeScript decorators provide a powerful meta-programming capability for adding cross-cutting concerns like logging, validation, and caching to your code through clean, declarative syntax. Combined with reflect-metadata, they enable advanced patterns like dependency injection and ORM mapping used in frameworks like Angular, NestJS, and TypeORM.
This post is licensed under CC BY 4.0 by the author.