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. π
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?
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?
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?
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?
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?
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
- Create a
@Retrydecorator factory that accepts:maxAttempts: Maximum number of retry attemptsdelayMs: Initial delay in millisecondsbackoffMultiplier: Multiplier for exponential backoff (default 2)
- On failure, wait
delayMs * (backoffMultiplier ^ attempt)before retrying - Log each retry attempt with attempt number and delay
- If all attempts fail, throw the last error
- If successful before max attempts, return immediately
π‘ Implementation Hints
- Use a
forloop to track attempts - Wrap method call in
try-catchto detect failures - Use
await new Promise(resolve => setTimeout(resolve, delay))for delays - Calculate delay:
delayMs * Math.pow(backoffMultiplier, attempt) - 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.