Decorators
Decorators are a way to add annotations and modify classes and their members. They're widely used in frameworks like Angular, NestJS, and TypeORM.
What Are Decorators?
A decorator is a function that receives information about the thing being decorated and can modify its behavior.
// Class decorator — a function that takes a class
function Logger(constructor: Function) {
console.log(`Class created: ${constructor.name}`);
}
// Simulating decorator behavior (since playground may not support @ syntax)
class UserService {
getUser(id: number) {
return { id, name: "Alice" };
}
}
// Apply decorator manually
Logger(UserService);
// Decorator factory — returns a decorator
function Component(options: { name: string; version: string }) {
return function(constructor: Function) {
console.log(`Registered component: ${options.name} v${options.version}`);
(constructor as any).componentName = options.name;
(constructor as any).version = options.version;
};
}
class AppHeader {
render() { return "<header>App</header>"; }
}
Component({ name: "AppHeader", version: "1.0" })(AppHeader);
console.log(`Component: ${(AppHeader as any).componentName}`);
Method Decorators
Method decorators wrap or modify class methods.
// Logging decorator
function log(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Calling ${key}(${args.join(", ")})`);
const result = original.apply(this, args);
console.log(` -> returned: ${JSON.stringify(result)}`);
return result;
};
}
// Timing decorator
function measure(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
const start = performance.now();
const result = original.apply(this, args);
const end = performance.now();
console.log(`${key} took ${(end - start).toFixed(3)}ms`);
return result;
};
}
class Calculator {
add(a: number, b: number): number { return a + b; }
multiply(a: number, b: number): number { return a * b; }
fibonacci(n: number): number {
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}
// Apply decorators manually
const calc = new Calculator();
const addDesc = Object.getOwnPropertyDescriptor(Calculator.prototype, "add")!;
log(Calculator.prototype, "add", addDesc);
Object.defineProperty(Calculator.prototype, "add", addDesc);
const fibDesc = Object.getOwnPropertyDescriptor(Calculator.prototype, "fibonacci")!;
measure(Calculator.prototype, "fibonacci", fibDesc);
Object.defineProperty(Calculator.prototype, "fibonacci", fibDesc);
calc.add(3, 4);
calc.fibonacci(20);
Validation Decorators
Decorators can enforce runtime validation.
// Simple validation system
const validations = new Map<string, Map<string, Function[]>>();
function validate(validator: (value: any) => boolean, message: string) {
return function(target: any, propertyKey: string) {
const className = target.constructor.name;
if (!validations.has(className)) {
validations.set(className, new Map());
}
const classValidations = validations.get(className)!;
if (!classValidations.has(propertyKey)) {
classValidations.set(propertyKey, []);
}
classValidations.get(propertyKey)!.push((value: any) => {
if (!validator(value)) throw new Error(`${propertyKey}: ${message}`);
});
};
}
class UserForm {
name: string = "";
email: string = "";
age: number = 0;
constructor(data: { name: string; email: string; age: number }) {
this.name = data.name;
this.email = data.email;
this.age = data.age;
}
}
// Register validations
validate((v: any) => typeof v === "string" && v.length >= 2, "must be at least 2 chars")(
UserForm.prototype, "name"
);
validate((v: any) => typeof v === "string" && v.includes("@"), "must be valid email")(
UserForm.prototype, "email"
);
validate((v: any) => typeof v === "number" && v >= 0 && v <= 150, "must be 0-150")(
UserForm.prototype, "age"
);
// Validate an instance
function validateInstance(instance: any): string[] {
const className = instance.constructor.name;
const classValidations = validations.get(className);
if (!classValidations) return [];
const errors: string[] = [];
classValidations.forEach((validators, field) => {
validators.forEach(validator => {
try { validator(instance[field]); }
catch (e: any) { errors.push(e.message); }
});
});
return errors;
}
const validUser = new UserForm({ name: "Alice", email: "alice@example.com", age: 30 });
console.log("Valid user errors:", validateInstance(validUser));
const invalidUser = new UserForm({ name: "A", email: "bad", age: -5 });
console.log("Invalid user errors:", validateInstance(invalidUser));
Decorator Factories
Factories let you configure decorator behavior with parameters.
// Retry decorator factory
function retry(maxAttempts: number) {
return function(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
let lastError: Error | undefined;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return original.apply(this, args);
} catch (error: any) {
lastError = error;
console.log(` Attempt ${attempt}/${maxAttempts} failed: ${error.message}`);
}
}
throw lastError;
};
};
}
class ApiClient {
private callCount = 0;
fetchData(): string {
this.callCount++;
if (this.callCount < 3) {
throw new Error("Network timeout");
}
return "Data received!";
}
}
const client = new ApiClient();
const fetchDesc = Object.getOwnPropertyDescriptor(ApiClient.prototype, "fetchData")!;
retry(5)(ApiClient.prototype, "fetchData", fetchDesc);
Object.defineProperty(ApiClient.prototype, "fetchData", fetchDesc);
try {
const result = client.fetchData();
console.log(`Result: ${result}`);
} catch (e: any) {
console.log(`Failed after all retries: ${e.message}`);
}
Try It Yourself
// Build a simple dependency injection container
class Container {
private services = new Map<string, any>();
register(name: string, factory: () => any): void {
this.services.set(name, factory);
}
resolve<T>(name: string): T {
const factory = this.services.get(name);
if (!factory) throw new Error(`Service '${name}' not registered`);
return factory();
}
}
// Services
class Database {
query(sql: string): string[] {
return [`Result for: ${sql}`];
}
}
class UserRepository {
constructor(private db: Database) {}
findAll(): string[] {
return this.db.query("SELECT * FROM users");
}
}
class UserController {
constructor(private repo: UserRepository) {}
listUsers(): void {
const users = this.repo.findAll();
console.log("Users:", users);
}
}
// Wire up dependencies
const container = new Container();
container.register("database", () => new Database());
container.register("userRepo", () => new UserRepository(container.resolve("database")));
container.register("userController", () => new UserController(container.resolve("userRepo")));
const controller = container.resolve<UserController>("userController");
controller.listUsers();
Key Takeaways
- Decorators are functions that modify classes, methods, or properties
- Decorator factories return decorators and accept configuration
- Method decorators receive the target, property key, and descriptor
- Decorators are widely used in frameworks like Angular and NestJS
- They enable cross-cutting concerns like logging, validation, and caching
- Combine multiple decorators — they compose from bottom to top
Pro Tip: Use decorators for cross-cutting concerns that span many classes: logging, caching, authentication checks, validation. Keep business logic in the methods themselves. If a decorator does too much, split it into multiple focused decorators that compose well.
Next Steps
You've built classes with decorators — but how do you verify they work correctly? Next, you'll learn how to write type-safe tests with assertion patterns, typed mocks, and compile-time type testing.
Next lesson
Testing
Write type-safe tests in TypeScript with assertion patterns, mocking, and test organization for reliable, maintainable test suites.
25 min