Chapter 6: Decorators
Authored by syscook.dev
What are Decorators in TypeScript?
Decorators are a special kind of declaration that can be attached to classes, methods, properties, or parameters. They provide a way to add metadata and modify behavior at design time, enabling powerful patterns like dependency injection, logging, validation, and more.
Key Concepts:
- Class Decorators: Applied to class declarations
- Method Decorators: Applied to method declarations
- Property Decorators: Applied to property declarations
- Parameter Decorators: Applied to parameter declarations
- Decorator Factories: Functions that return decorator functions
- Metadata Reflection: Accessing decorator metadata at runtime
Why Use Decorators?
1. Cross-Cutting Concerns
Decorators help separate cross-cutting concerns from business logic.
// Without decorators - mixed concerns
class UserService {
async getUser(id: number) {
console.log(`Getting user ${id}`); // Logging mixed with business logic
const startTime = Date.now();
try {
const user = await this.fetchUser(id);
console.log(`User ${id} retrieved in ${Date.now() - startTime}ms`);
return user;
} catch (error) {
console.error(`Error getting user ${id}:`, error);
throw error;
}
}
}
// With decorators - separated concerns
function log(target: any, propertyName: string, descriptor: PropertyDescriptor) {
const method = descriptor.value;
descriptor.value = async function (...args: any[]) {
console.log(`Calling ${propertyName} with args:`, args);
const startTime = Date.now();
try {
const result = await method.apply(this, args);
console.log(`${propertyName} completed in ${Date.now() - startTime}ms`);
return result;
} catch (error) {
console.error(`Error in ${propertyName}:`, error);
throw error;
}
};
}
class UserService {
@log
async getUser(id: number) {
return await this.fetchUser(id);
}
}
2. Framework Integration
Decorators are essential for frameworks like Angular, NestJS, and TypeORM.
// Angular-style decorators
@Component({
selector: 'app-user',
template: '<div>{{user.name}}</div>'
})
export class UserComponent {
@Input() user: User;
@Output() userChange = new EventEmitter<User>();
@HostListener('click')
onClick() {
this.userChange.emit(this.user);
}
}
// NestJS-style decorators
@Controller('users')
export class UserController {
constructor(private userService: UserService) {}
@Get(':id')
@UseGuards(AuthGuard)
async getUser(@Param('id') id: string) {
return this.userService.getUser(id);
}
}
How to Use Decorators?
1. Class Decorators
Basic Class Decorator
// Simple class decorator
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class User {
name: string;
email: string;
constructor(name: string, email: string) {
this.name = name;
this.email = email;
}
}
// Class decorator with parameters (decorator factory)
function Component(config: { selector: string; template: string }) {
return function (constructor: Function) {
constructor.prototype.selector = config.selector;
constructor.prototype.template = config.template;
};
}
@Component({
selector: 'app-user',
template: '<div>User Component</div>'
})
class UserComponent {
// Component logic
}
console.log("User class sealed:", Object.isSealed(User));
console.log("UserComponent selector:", (UserComponent as any).prototype.selector);
Output:
User class sealed: true
UserComponent selector: app-user
Advanced Class Decorator
// Class decorator that adds methods
function withTimestamps<T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
createdAt = new Date();
updatedAt = new Date();
updateTimestamp() {
this.updatedAt = new Date();
}
};
}
@withTimestamps
class Product {
name: string;
price: number;
constructor(name: string, price: number) {
this.name = name;
this.price = price;
}
}
const product = new Product("Laptop", 999);
console.log("Product created at:", product.createdAt);
console.log("Product updated at:", product.updatedAt);
product.updateTimestamp();
console.log("Product updated at (after update):", product.updatedAt);
Output:
Product created at: 2024-01-15T10:30:00.000Z
Product updated at: 2024-01-15T10:30:00.000Z
Product updated at (after update): 2024-01-15T10:30:00.000Z
2. Method Decorators
Basic Method Decorator
// Method decorator for logging
function log(target: any, propertyName: string, descriptor: PropertyDescriptor) {
const method = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyName} with arguments:`, args);
const result = method.apply(this, args);
console.log(`${propertyName} returned:`, result);
return result;
};
}
// Method decorator for validation
function validate(target: any, propertyName: string, descriptor: PropertyDescriptor) {
const method = descriptor.value;
descriptor.value = function (...args: any[]) {
// Simple validation example
if (args.some(arg => arg === null || arg === undefined)) {
throw new Error(`Invalid arguments for ${propertyName}`);
}
return method.apply(this, args);
};
}
class Calculator {
@log
@validate
add(a: number, b: number): number {
return a + b;
}
@log
multiply(a: number, b: number): number {
return a * b;
}
}
const calc = new Calculator();
console.log("Result:", calc.add(5, 3));
console.log("Result:", calc.multiply(4, 7));
Output:
Calling add with arguments: [5, 3]
add returned: 8
Result: 8
Calling multiply with arguments: [4, 7]
multiply returned: 28
Result: 28
Method Decorator with Parameters
// Decorator factory for method timing
function timing(threshold: number = 100) {
return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
const method = descriptor.value;
descriptor.value = async function (...args: any[]) {
const startTime = Date.now();
const result = await method.apply(this, args);
const duration = Date.now() - startTime;
if (duration > threshold) {
console.warn(`Slow method ${propertyName}: ${duration}ms (threshold: ${threshold}ms)`);
} else {
console.log(`Method ${propertyName} completed in ${duration}ms`);
}
return result;
};
};
}
class DataService {
@timing(50)
async fetchData(): Promise<any> {
// Simulate async operation
await new Promise(resolve => setTimeout(resolve, 30));
return { data: "Sample data" };
}
@timing(50)
async slowOperation(): Promise<any> {
// Simulate slow operation
await new Promise(resolve => setTimeout(resolve, 100));
return { data: "Slow data" };
}
}
const dataService = new DataService();
dataService.fetchData().then(result => {
console.log("Fast operation result:", result);
});
dataService.slowOperation().then(result => {
console.log("Slow operation result:", result);
});
Output:
Method fetchData completed in 30ms
Fast operation result: { data: 'Sample data' }
Slow method slowOperation: 100ms (threshold: 50ms)
Slow operation result: { data: 'Slow data' }
3. Property Decorators
Basic Property Decorator
// Property decorator for formatting
function format(formatFn: (value: any) => any) {
return function (target: any, propertyName: string) {
let value = target[propertyName];
const getter = function () {
return value;
};
const setter = function (newValue: any) {
value = formatFn(newValue);
};
Object.defineProperty(target, propertyName, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
};
}
// Property decorator for validation
function required(target: any, propertyName: string) {
const validators = target.constructor._validators || (target.constructor._validators = []);
validators.push({
property: propertyName,
validator: (value: any) => value != null && value !== ''
});
}
class User {
@required
@format((value: string) => value?.trim().toLowerCase())
email: string;
@format((value: string) => value?.trim())
name: string;
constructor(email: string, name: string) {
this.email = email;
this.name = name;
}
validate(): boolean {
const validators = (this.constructor as any)._validators || [];
return validators.every((validator: any) => validator.validator(this[validator.property]));
}
}
const user = new User(" [email protected] ", " John Doe ");
console.log("Email:", user.email);
console.log("Name:", user.name);
console.log("Valid:", user.validate());
Output:
Email: [email protected]
Name: John Doe
Valid: true
4. Parameter Decorators
Basic Parameter Decorator
// Parameter decorator for validation
function validateParam(target: any, propertyName: string, parameterIndex: number) {
const existingValidators = target.constructor._paramValidators || (target.constructor._paramValidators = []);
existingValidators.push({
method: propertyName,
parameterIndex,
validator: (value: any) => value != null && value !== ''
});
}
// Parameter decorator for logging
function logParam(target: any, propertyName: string, parameterIndex: number) {
const existingLoggers = target.constructor._paramLoggers || (target.constructor._paramLoggers = []);
existingLoggers.push({
method: propertyName,
parameterIndex
});
}
class UserService {
getUser(@validateParam @logParam id: number, @validateParam name: string) {
console.log(`Getting user with ID: ${id}, Name: ${name}`);
return { id, name };
}
}
// Usage
const userService = new UserService();
const user = userService.getUser(1, "John");
console.log("User:", user);
Output:
Getting user with ID: 1, Name: John
User: { id: 1, name: 'John' }
5. Decorator Composition
// Multiple decorators on a single target
function readonly(target: any, propertyName: string, descriptor: PropertyDescriptor) {
descriptor.writable = false;
}
function enumerable(value: boolean) {
return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
};
}
class Example {
@readonly
@enumerable(false)
secret: string = "secret value";
@enumerable(true)
public: string = "public value";
}
const example = new Example();
console.log("Object keys:", Object.keys(example));
console.log("Secret:", example.secret);
// example.secret = "new value"; // Error: Cannot assign to read only property
Output:
Object keys: ['public']
Secret: secret value
Practical Examples
1. Dependency Injection System
// Simple dependency injection system using decorators
const dependencies = new Map();
function Injectable(target: any) {
dependencies.set(target.name, target);
return target;
}
function Inject(token: string) {
return function (target: any, propertyName: string, parameterIndex: number) {
const existingTokens = target.constructor._injectTokens || (target.constructor._injectTokens = []);
existingTokens[parameterIndex] = token;
};
}
class Container {
private instances = new Map();
resolve<T>(token: string): T {
if (this.instances.has(token)) {
return this.instances.get(token);
}
const constructor = dependencies.get(token);
if (!constructor) {
throw new Error(`No dependency found for token: ${token}`);
}
const instance = new constructor();
this.instances.set(token, instance);
return instance;
}
}
// Usage
@Injectable
class DatabaseService {
connect() {
console.log("Database connected");
}
}
@Injectable
class UserRepository {
constructor(@Inject("DatabaseService") private db: DatabaseService) {}
findUser(id: number) {
this.db.connect();
return { id, name: "John" };
}
}
@Injectable
class UserService {
constructor(@Inject("UserRepository") private userRepo: UserRepository) {}
getUser(id: number) {
return this.userRepo.findUser(id);
}
}
// Manual dependency injection (in real frameworks, this is automatic)
const container = new Container();
const userService = container.resolve<UserService>("UserService");
const user = userService.getUser(1);
console.log("User:", user);
Output:
Database connected
User: { id: 1, name: 'John' }
2. API Route Decorators
// API route decorators
const routes: any[] = [];
function Controller(basePath: string) {
return function (target: any) {
target.prototype.basePath = basePath;
};
}
function Get(path: string) {
return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
routes.push({
method: 'GET',
path: target.prototype.basePath + path,
handler: descriptor.value,
controller: target.constructor.name
});
};
}
function Post(path: string) {
return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
routes.push({
method: 'POST',
path: target.prototype.basePath + path,
handler: descriptor.value,
controller: target.constructor.name
});
};
}
function Body(target: any, propertyName: string, parameterIndex: number) {
const existingParams = target.constructor._bodyParams || (target.constructor._bodyParams = []);
existingParams[parameterIndex] = 'body';
}
@Controller('/api/users')
class UserController {
@Get('/')
getAllUsers() {
return { users: [{ id: 1, name: "John" }] };
}
@Get('/:id')
getUserById(id: number) {
return { id, name: "John" };
}
@Post('/')
createUser(@Body userData: any) {
return { id: 2, ...userData };
}
}
// Display registered routes
console.log("Registered routes:");
routes.forEach(route => {
console.log(`${route.method} ${route.path} -> ${route.controller}.${route.handler.name}`);
});
Output:
Registered routes:
GET /api/users/ -> UserController.getAllUsers
GET /api/users/:id -> UserController.getUserById
POST /api/users/ -> UserController.createUser
Best Practices
1. Use Decorators for Cross-Cutting Concerns
// Good: Use decorators for logging, validation, caching
@Log
@Validate
@Cache(300)
async getUser(id: number) {
return this.userRepository.findById(id);
}
2. Keep Decorators Simple and Focused
// Good: Single responsibility decorators
function Log(target: any, propertyName: string, descriptor: PropertyDescriptor) {
// Only handles logging
}
function Validate(target: any, propertyName: string, descriptor: PropertyDescriptor) {
// Only handles validation
}
3. Use Decorator Factories for Configuration
// Good: Configurable decorators
function Cache(ttl: number) {
return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
// Cache implementation with TTL
};
}
4. Document Decorator Behavior
/**
* Logs method calls with timing information
* @param threshold - Warning threshold in milliseconds
*/
function timing(threshold: number = 100) {
return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
// Implementation
};
}
Common Pitfalls and Solutions
1. Decorator Execution Order
// Decorators execute from bottom to top
class Example {
@decoratorA
@decoratorB
method() {}
}
// Execution order: decoratorB, then decoratorA
2. Metadata Reflection API
// Enable experimental decorators and metadata in tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
// Install reflect-metadata
// npm install reflect-metadata
3. Decorator Context
// Be careful with 'this' context in decorators
function log(target: any, propertyName: string, descriptor: PropertyDescriptor) {
const method = descriptor.value;
descriptor.value = function (...args: any[]) {
// Use function() to preserve 'this' context
return method.apply(this, args);
};
}
Conclusion
Decorators in TypeScript provide powerful tools for adding metadata and modifying behavior at design time. By understanding:
- What decorators are and their types
- Why they're useful for cross-cutting concerns and framework integration
- How to create and use class, method, property, and parameter decorators
You can create sophisticated applications with clean separation of concerns. Decorators enable powerful patterns like dependency injection, aspect-oriented programming, and framework integration while keeping your business logic clean and focused.
Next Steps
- Practice creating custom decorators
- Explore framework-specific decorator patterns
- Learn about metadata reflection API
- Move on to Chapter 7: Utility Types
This tutorial is part of the TypeScript Mastery series by syscook.dev