TypeScript Intermediate
Course 2 · Chapter 3 · Decorators & Metadata
✨ Decorators & Metadata
Decorators are TypeScript's way of adding metadata and behavior to classes, methods, properties, and parameters. They're powerful (and experimental), enabling frameworks like Angular to work their magic. This chapter teaches you how to read, write, and reason about decorators.
⚠️ Note: Decorators are an experimental feature. To use them, enable "experimentalDecorators": true in tsconfig.json. They're currently a TC39 proposal but already widely used in production.
What Are Decorators?
A decorator is a function that modifies a class, method, property, or parameter. It runs at class definition time and can inspect or alter the target. Decorators are prefixed with @:
@Log
class User {
@Validate
name: string;
@Deprecated
oldMethod() { }
}
class User { }
User = Log(User);
Key insight: A decorator is just a function that receives the target (class, method, etc.) and can return a modified version of it.
🏛️ Class Decorators
A class decorator receives the constructor function and can replace or enhance it:
function Sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@Sealed
class User {
name = "Alice";
}
Practical: Class Decorator for Logging
function Log(constructor: Function) {
return class extends constructor {
constructor(...args: any[]) {
console.log(`Creating instance of ${constructor.name}`);
super(...args);
}
};
}
@Log
class Database {
constructor(url: string) {
console.log(`Connecting to ${url}`);
}
}
new Database("postgres://...");
🏷️ Property Decorators
A property decorator receives the class prototype and the property name. It can't return anything (it modifies in place):
function Uppercase(target: any, propertyKey: string) {
let value: string;
const getter = () => value;
const setter = (newValue: string) => {
value = newValue.toUpperCase();
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
class User {
@Uppercase
name: string = "";
}
const user = new User();
user.name = "alice";
console.log(user.name);
Signature
(target: any, propertyKey: string)
Use Cases
Validation, transformation, metadata attachment, lazy initialization.
Limitation
Can't return anything. Modifies the class prototype in place.
Runs When?
At class definition time, not when instances are created.
🎯 Method Decorators
A method decorator receives the prototype, method name, and a property descriptor. It can wrap or replace the method:
function TimeIt(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
const start = Date.now();
const result = originalMethod.apply(this, args);
const elapsed = Date.now() - start;
console.log(`${propertyKey} took ${elapsed}ms`);
return result;
};
return descriptor;
}
class Calculator {
@TimeIt
fibonacci(n: number): number {
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}
const calc = new Calculator();
calc.fibonacci(10);
Signature
(target: any, propertyKey: string, descriptor: PropertyDescriptor)
Descriptor.value
The original method function. Replace or wrap it to modify behavior.
Common Patterns
Logging, timing, caching, error handling, authentication checks.
Return Value
Return the (modified) descriptor to finalize the change.
🔗 Parameter Decorators
A parameter decorator receives the prototype, method name, and parameter index. Typically used for metadata (requires reflecting metadata):
function Required(target: any, propertyKey: string | undefined, parameterIndex: number) {
const existingMetadata = Reflect.getOwnMetadata("required", target, propertyKey) || [];
existingMetadata[parameterIndex] = true;
Reflect.defineMetadata("required", existingMetadata, target, propertyKey);
}
class User {
create(@Required name: string, @Required email: string) {
console.log(`Creating user: ${name} (${email})`);
}
}
const metadata = Reflect.getOwnMetadata("required", User.prototype, "create");
Note: Parameter decorators alone can't enforce behavior. They're typically used with method decorators that read the metadata and validate.
⚙️ Decorator Factories: Parameterized Decorators
To pass options to a decorator, wrap it in a factory function:
function MinLength(min: number) {
return function(target: any, propertyKey: string) {
let value: string;
const setter = (newValue: string) => {
if (newValue.length < min) {
throw new Error(`${propertyKey} must be at least ${min} characters`);
}
value = newValue;
};
Object.defineProperty(target, propertyKey, { set: setter });
};
}
class User {
@MinLength(3)
name: string = "";
}
const user = new User();
user.name = "ab";
🏗️ Real-World Pattern: Validation Decorator
Complete Validation System
function Validate(rules: { [key: string]: (value: any) => boolean }) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
for (const [param, rule] of Object.entries(rules)) {
const value = args[0];
if (!rule(value)) {
throw new Error(`Validation failed for ${param}`);
}
}
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class UserService {
@Validate({
email: (v) => typeof v === "string" && v.includes("@")
})
createUser(email: string) {
console.log(`User created: ${email}`);
}
}
const service = new UserService();
service.createUser("alice@example.com");
service.createUser("invalid");
💻 Coding Challenges
Challenge 1: Method Logging Decorator
Create a method decorator that logs when a method is called and what it returns. Apply it to a sample class method.
Goal: Practice method decorators and descriptor manipulation.
→ Solution
Challenge 2: Property Validator Decorator
Create a property decorator that enforces a constraint (e.g., email must be a valid format). Use it on a class property and test it.
Goal: Build property decorators with validation logic.
→ Solution
Challenge 3: Decorator Factory
Create a decorator factory (decorator with parameters) that limits method calls to N times. Apply it to a method and verify it stops after the limit.
Goal: Understand how decorator factories work and parameterize decorators.
→ Solution
⚠️ Gotcha: Decorators Run at Definition Time
Decorators execute when the class is defined, not when instances are created. If you need per-instance behavior, wrap the original method inside a decorator factory or use a method decorator that returns a wrapped function. Also, always remember: TypeScript's decorator output depends on your tsconfig settings — test with experimentalDecorators enabled.
🎯 What's Next
With decorators mastered, we'll explore Async Patterns — mastering Promises, async/await, error handling, and concurrent operations for real-world applications.