JavaScript Decorators: Enhancing Classes and Methods
Master JavaScript decorators for adding metadata and modifying class behavior. Learn decorator syntax, patterns, and practical use cases with TypeScript.
Decorators are a stage 3 proposal for JavaScript that enable declarative modification and annotation of classes and their members. They provide a clean syntax for common patterns like logging, validation, memoization, and dependency injection.
Understanding Decorators
Decorators are functions that can modify classes, methods, properties, or parameters at definition time. They use the @ symbol followed by the decorator name.
Basic Decorator Syntax
// TypeScript/experimental JavaScript syntax
@sealed
class BugReport {
@readonly
type = "report";
@validate
title: string;
@log
print() {
console.log(`Type: ${this.type}`);
console.log(`Title: ${this.title}`);
}
}
// Decorator implementations
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
function readonly(target: any, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
writable: false,
configurable: false
});
}
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Calling ${propertyKey} with arguments:`, args);
const result = originalMethod.apply(this, args);
console.log(`${propertyKey} returned:`, result);
return result;
};
return descriptor;
}
function validate(target: any, propertyKey: string) {
let value = target[propertyKey];
const getter = () => value;
const setter = (newVal: string) => {
if (!newVal || newVal.trim().length === 0) {
throw new Error(`${propertyKey} cannot be empty`);
}
value = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
Types of Decorators
Class Decorators
Class decorators are applied to class constructors and can modify or replace the class definition.
// Basic class decorator
function Component(options: ComponentOptions) {
return function <T extends { new(...args: any[]): {} }>(constructor: T) {
return class extends constructor {
selector = options.selector;
template = options.template;
constructor(...args: any[]) {
super(...args);
console.log(`Component ${options.selector} created`);
}
};
};
}
interface ComponentOptions {
selector: string;
template: string;
}
@Component({
selector: 'app-header',
template: '<h1>{{title}}</h1>'
})
class HeaderComponent {
title = 'My App';
}
// Singleton decorator
function Singleton<T extends { new(...args: any[]): {} }>(constructor: T) {
let instance: T;
return class extends constructor {
constructor(...args: any[]) {
if (instance) {
return instance;
}
super(...args);
instance = this;
}
};
}
@Singleton
class DatabaseConnection {
private connected = false;
connect() {
if (!this.connected) {
console.log('Connecting to database...');
this.connected = true;
}
}
}
// Multiple decorators
function Injectable(target: Function) {
target.prototype.injected = true;
}
function Controller(path: string) {
return function(target: Function) {
target.prototype.basePath = path;
};
}
@Injectable
@Controller('/api/users')
class UserController {
// Controller implementation
}
Method Decorators
Method decorators can modify method behavior, add logging, implement caching, or handle errors.
// Logging decorator
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`[${new Date().toISOString()}] ${propertyKey} called with:`, args);
try {
const result = originalMethod.apply(this, args);
console.log(`[${new Date().toISOString()}] ${propertyKey} returned:`, result);
return result;
} catch (error) {
console.error(`[${new Date().toISOString()}] ${propertyKey} threw:`, error);
throw error;
}
};
return descriptor;
}
// Memoization decorator
function Memoize(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
const cache = new Map();
descriptor.value = function(...args: any[]) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log(`Cache hit for ${propertyKey}`);
return cache.get(key);
}
const result = originalMethod.apply(this, args);
cache.set(key, result);
return result;
};
return descriptor;
}
// Debounce decorator
function Debounce(delay: number) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
let timeout: NodeJS.Timeout;
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
clearTimeout(timeout);
return new Promise((resolve) => {
timeout = setTimeout(() => {
resolve(originalMethod.apply(this, args));
}, delay);
});
};
return descriptor;
};
}
// Error handling decorator
function CatchError(errorHandler?: (error: Error) => void) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function(...args: any[]) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
if (errorHandler) {
errorHandler(error);
} else {
console.error(`Error in ${propertyKey}:`, error);
}
return null;
}
};
return descriptor;
};
}
class MathService {
@Log
@Memoize
fibonacci(n: number): number {
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
@Debounce(1000)
@Log
async search(query: string) {
console.log(`Searching for: ${query}`);
// Simulate API call
return `Results for ${query}`;
}
@CatchError((error) => console.log('Custom error handler:', error))
async riskyOperation() {
throw new Error('Something went wrong!');
}
}
Property Decorators
Property decorators can add metadata or modify property behavior.
// Validation decorators
const validationRules = new Map<any, Map<string, ValidationRule[]>>();
interface ValidationRule {
validate: (value: any) => boolean;
message: string;
}
function Required(target: any, propertyKey: string) {
addValidationRule(target, propertyKey, {
validate: (value) => value != null && value !== '',
message: `${propertyKey} is required`
});
}
function MinLength(length: number) {
return function(target: any, propertyKey: string) {
addValidationRule(target, propertyKey, {
validate: (value) => value && value.length >= length,
message: `${propertyKey} must be at least ${length} characters`
});
};
}
function MaxLength(length: number) {
return function(target: any, propertyKey: string) {
addValidationRule(target, propertyKey, {
validate: (value) => !value || value.length <= length,
message: `${propertyKey} must be at most ${length} characters`
});
};
}
function Email(target: any, propertyKey: string) {
addValidationRule(target, propertyKey, {
validate: (value) => !value || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
message: `${propertyKey} must be a valid email`
});
}
function addValidationRule(target: any, propertyKey: string, rule: ValidationRule) {
if (!validationRules.has(target)) {
validationRules.set(target, new Map());
}
const rules = validationRules.get(target)!;
if (!rules.has(propertyKey)) {
rules.set(propertyKey, []);
}
rules.get(propertyKey)!.push(rule);
}
// Observable property decorator
function Observable(target: any, propertyKey: string) {
const privateKey = `_${propertyKey}`;
const observersKey = `${propertyKey}_observers`;
target[observersKey] = [];
Object.defineProperty(target, propertyKey, {
get() {
return this[privateKey];
},
set(value: any) {
const oldValue = this[privateKey];
this[privateKey] = value;
// Notify observers
this[observersKey].forEach((observer: Function) => {
observer.call(this, value, oldValue, propertyKey);
});
},
enumerable: true,
configurable: true
});
// Add subscribe method
target[`subscribe_${propertyKey}`] = function(observer: Function) {
this[observersKey].push(observer);
// Return unsubscribe function
return () => {
const index = this[observersKey].indexOf(observer);
if (index > -1) {
this[observersKey].splice(index, 1);
}
};
};
}
class User {
@Required
@MinLength(3)
@MaxLength(50)
username: string;
@Required
@Email
email: string;
@Observable
status: 'active' | 'inactive' = 'active';
validate(): string[] {
const errors: string[] = [];
const rules = validationRules.get(Object.getPrototypeOf(this));
if (rules) {
rules.forEach((ruleList, property) => {
const value = (this as any)[property];
ruleList.forEach(rule => {
if (!rule.validate(value)) {
errors.push(rule.message);
}
});
});
}
return errors;
}
}
Parameter Decorators
Parameter decorators can add metadata about method parameters.
// Dependency injection
const injectableMetadata = new Map<any, Map<string, any[]>>();
function Inject(token: any) {
return function(target: any, propertyKey: string | symbol, parameterIndex: number) {
if (!injectableMetadata.has(target)) {
injectableMetadata.set(target, new Map());
}
const metadata = injectableMetadata.get(target)!;
if (!metadata.has(propertyKey as string)) {
metadata.set(propertyKey as string, []);
}
metadata.get(propertyKey as string)![parameterIndex] = token;
};
}
// Validation parameter decorator
function ValidateParam(validator: (value: any) => boolean, message: string) {
return function(target: any, propertyKey: string, parameterIndex: number) {
const originalMethod = target[propertyKey];
target[propertyKey] = function(...args: any[]) {
if (!validator(args[parameterIndex])) {
throw new Error(`Parameter ${parameterIndex} validation failed: ${message}`);
}
return originalMethod.apply(this, args);
};
};
}
// Service tokens
const DATABASE_TOKEN = Symbol('DATABASE');
const LOGGER_TOKEN = Symbol('LOGGER');
class UserService {
constructor(
@Inject(DATABASE_TOKEN) private database: any,
@Inject(LOGGER_TOKEN) private logger: any
) {}
findUser(
@ValidateParam((id) => typeof id === 'number' && id > 0, 'ID must be positive number')
id: number
) {
this.logger.log(`Finding user ${id}`);
return this.database.query(`SELECT * FROM users WHERE id = ${id}`);
}
}
Advanced Decorator Patterns
Decorator Factories
// Configurable decorator factory
function Retry(options: RetryOptions = {}) {
const {
attempts = 3,
delay = 1000,
backoff = 2,
onError
} = options;
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function(...args: any[]) {
let lastError: Error;
for (let i = 0; i < attempts; i++) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
lastError = error;
if (onError) {
onError(error, i + 1);
}
if (i < attempts - 1) {
const waitTime = delay * Math.pow(backoff, i);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
}
}
throw lastError!;
};
return descriptor;
};
}
interface RetryOptions {
attempts?: number;
delay?: number;
backoff?: number;
onError?: (error: Error, attempt: number) => void;
}
// Rate limiting decorator
function RateLimit(options: RateLimitOptions) {
const { limit, window } = options;
const calls = new Map<string, number[]>();
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
const key = `${this.constructor.name}.${propertyKey}`;
const now = Date.now();
if (!calls.has(key)) {
calls.set(key, []);
}
const timestamps = calls.get(key)!;
// Remove old timestamps
const cutoff = now - window;
calls.set(key, timestamps.filter(t => t > cutoff));
if (timestamps.length >= limit) {
throw new Error(`Rate limit exceeded for ${key}`);
}
timestamps.push(now);
return originalMethod.apply(this, args);
};
return descriptor;
};
}
interface RateLimitOptions {
limit: number;
window: number; // milliseconds
}
// Conditional decorator
function When(condition: (target: any) => boolean) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
if (!condition(this)) {
throw new Error(`Condition not met for ${propertyKey}`);
}
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class ApiService {
private isAuthenticated = false;
@Retry({
attempts: 3,
delay: 500,
onError: (error, attempt) => console.log(`Attempt ${attempt} failed:`, error)
})
async fetchData(url: string) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
@RateLimit({ limit: 10, window: 60000 }) // 10 calls per minute
@When((instance) => instance.isAuthenticated)
async postData(data: any) {
console.log('Posting data:', data);
// API call implementation
}
}
Metadata and Reflection
// Metadata decorator system
const metadata = new WeakMap<any, Map<string | symbol, any>>();
function SetMetadata(key: string, value: any) {
return function(target: any, propertyKey?: string | symbol) {
if (!metadata.has(target)) {
metadata.set(target, new Map());
}
const metaKey = propertyKey || 'class';
const meta = metadata.get(target)!;
if (!meta.has(metaKey)) {
meta.set(metaKey, {});
}
meta.get(metaKey)[key] = value;
};
}
function GetMetadata(target: any, propertyKey?: string | symbol, key?: string) {
const meta = metadata.get(target);
if (!meta) return undefined;
const metaKey = propertyKey || 'class';
const propMeta = meta.get(metaKey);
return key ? propMeta?.[key] : propMeta;
}
// HTTP method decorators
function HttpMethod(method: string, path: string) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
SetMetadata('method', method)(target, propertyKey);
SetMetadata('path', path)(target, propertyKey);
return descriptor;
};
}
const Get = (path: string) => HttpMethod('GET', path);
const Post = (path: string) => HttpMethod('POST', path);
const Put = (path: string) => HttpMethod('PUT', path);
const Delete = (path: string) => HttpMethod('DELETE', path);
// Route controller
@SetMetadata('basePath', '/api/products')
class ProductController {
@Get('/')
@SetMetadata('roles', ['user', 'admin'])
async getProducts() {
return [{ id: 1, name: 'Product 1' }];
}
@Get('/:id')
@SetMetadata('roles', ['user', 'admin'])
async getProduct(id: string) {
return { id, name: `Product ${id}` };
}
@Post('/')
@SetMetadata('roles', ['admin'])
async createProduct(data: any) {
return { id: Date.now(), ...data };
}
@Put('/:id')
@SetMetadata('roles', ['admin'])
async updateProduct(id: string, data: any) {
return { id, ...data };
}
@Delete('/:id')
@SetMetadata('roles', ['admin'])
async deleteProduct(id: string) {
return { deleted: true, id };
}
}
// Router registration
function registerRoutes(controller: any) {
const basePath = GetMetadata(controller, undefined, 'basePath') || '';
const prototype = controller.prototype;
Object.getOwnPropertyNames(prototype).forEach(propertyKey => {
if (propertyKey === 'constructor') return;
const method = GetMetadata(prototype, propertyKey, 'method');
const path = GetMetadata(prototype, propertyKey, 'path');
const roles = GetMetadata(prototype, propertyKey, 'roles');
if (method && path) {
console.log(`Registering ${method} ${basePath}${path}`);
console.log(`Required roles:`, roles);
// Register with actual router here
}
});
}
registerRoutes(ProductController);
Composition and Stacking
// Compose decorators
function compose(...decorators: MethodDecorator[]): MethodDecorator {
return function(target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
decorators.reverse().forEach(decorator => {
descriptor = decorator(target, propertyKey, descriptor) || descriptor;
});
return descriptor;
};
}
// Timing decorator
function Time(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`);
return result;
} catch (error) {
const duration = performance.now() - start;
console.log(`${propertyKey} failed after ${duration.toFixed(2)}ms`);
throw error;
}
};
return descriptor;
}
// Cache with TTL
function CacheWithTTL(ttl: number) {
const cache = new Map<string, { value: any; expires: number }>();
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
const key = JSON.stringify(args);
const cached = cache.get(key);
if (cached && cached.expires > Date.now()) {
console.log(`Cache hit for ${propertyKey}`);
return cached.value;
}
const result = originalMethod.apply(this, args);
cache.set(key, {
value: result,
expires: Date.now() + ttl
});
return result;
};
return descriptor;
};
}
// Transform result decorator
function Transform(transformer: (result: any) => any) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function(...args: any[]) {
const result = await originalMethod.apply(this, args);
return transformer(result);
};
return descriptor;
};
}
class DataService {
@compose(
Time,
CacheWithTTL(60000),
Transform(result => ({ data: result, timestamp: Date.now() })),
Log
)
async fetchUserData(userId: number) {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
return { id: userId, name: `User ${userId}` };
}
}
Real-World Examples
Dependency Injection Container
// Simple DI container
class Container {
private services = new Map<any, any>();
private factories = new Map<any, () => any>();
register<T>(token: any, factory: () => T): void {
this.factories.set(token, factory);
}
registerSingleton<T>(token: any, instance: T): void {
this.services.set(token, instance);
}
resolve<T>(token: any): T {
if (this.services.has(token)) {
return this.services.get(token);
}
if (this.factories.has(token)) {
const instance = this.factories.get(token)!();
this.services.set(token, instance);
return instance;
}
throw new Error(`No provider for ${token.toString()}`);
}
}
const container = new Container();
// Injectable decorator
function Injectable(target: any) {
const paramTypes = Reflect.getMetadata('design:paramtypes', target) || [];
const tokens = injectableMetadata.get(target)?.get('constructor') || [];
container.register(target, () => {
const deps = paramTypes.map((type: any, index: number) => {
const token = tokens[index] || type;
return container.resolve(token);
});
return new target(...deps);
});
}
// Example services
interface IDatabase {
query(sql: string): any[];
}
interface ILogger {
log(message: string): void;
}
@Injectable
class ConsoleLogger implements ILogger {
log(message: string): void {
console.log(`[${new Date().toISOString()}] ${message}`);
}
}
@Injectable
class MockDatabase implements IDatabase {
constructor(@Inject('LOGGER') private logger: ILogger) {}
query(sql: string): any[] {
this.logger.log(`Executing query: ${sql}`);
return [];
}
}
// Register services
container.registerSingleton('LOGGER', new ConsoleLogger());
container.register('DATABASE', () => container.resolve(MockDatabase));
// Usage
const db = container.resolve<IDatabase>('DATABASE');
db.query('SELECT * FROM users');
ORM-like Decorators
// Entity decorators
const entityMetadata = new Map<any, EntityMetadata>();
interface EntityMetadata {
tableName: string;
columns: Map<string, ColumnMetadata>;
}
interface ColumnMetadata {
name: string;
type: string;
primaryKey?: boolean;
nullable?: boolean;
default?: any;
}
function Entity(tableName: string) {
return function(target: any) {
entityMetadata.set(target, {
tableName,
columns: new Map()
});
};
}
function Column(options: Partial<ColumnMetadata> = {}) {
return function(target: any, propertyKey: string) {
const constructor = target.constructor;
if (!entityMetadata.has(constructor)) {
entityMetadata.set(constructor, {
tableName: constructor.name.toLowerCase(),
columns: new Map()
});
}
const metadata = entityMetadata.get(constructor)!;
metadata.columns.set(propertyKey, {
name: options.name || propertyKey,
type: options.type || 'VARCHAR(255)',
...options
});
};
}
function PrimaryKey(target: any, propertyKey: string) {
Column({ primaryKey: true, type: 'INTEGER' })(target, propertyKey);
}
function CreatedAt(target: any, propertyKey: string) {
Column({ type: 'TIMESTAMP', default: 'CURRENT_TIMESTAMP' })(target, propertyKey);
}
function UpdatedAt(target: any, propertyKey: string) {
Column({
type: 'TIMESTAMP',
default: 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'
})(target, propertyKey);
}
// Example entity
@Entity('users')
class User {
@PrimaryKey
id: number;
@Column({ type: 'VARCHAR(100)', nullable: false })
username: string;
@Column({ type: 'VARCHAR(255)', nullable: false })
email: string;
@Column({ type: 'TEXT', nullable: true })
bio?: string;
@CreatedAt
createdAt: Date;
@UpdatedAt
updatedAt: Date;
}
// Generate SQL from decorators
function generateCreateTableSQL(entity: any): string {
const metadata = entityMetadata.get(entity);
if (!metadata) {
throw new Error('Entity metadata not found');
}
const columns: string[] = [];
let primaryKey: string | undefined;
metadata.columns.forEach((column, property) => {
let sql = `${column.name} ${column.type}`;
if (column.primaryKey) {
sql += ' PRIMARY KEY AUTO_INCREMENT';
primaryKey = column.name;
}
if (column.nullable === false) {
sql += ' NOT NULL';
}
if (column.default !== undefined) {
sql += ` DEFAULT ${column.default}`;
}
columns.push(sql);
});
return `CREATE TABLE ${metadata.tableName} (\n ${columns.join(',\n ')}\n);`;
}
console.log(generateCreateTableSQL(User));
Validation Framework
// Comprehensive validation system
class Validator {
private static schemas = new Map<any, ValidationSchema>();
static addRule(target: any, property: string, rule: ValidationRule) {
if (!this.schemas.has(target)) {
this.schemas.set(target, { rules: new Map() });
}
const schema = this.schemas.get(target)!;
if (!schema.rules.has(property)) {
schema.rules.set(property, []);
}
schema.rules.get(property)!.push(rule);
}
static validate(instance: any): ValidationResult {
const errors: ValidationError[] = [];
const constructor = Object.getPrototypeOf(instance);
const schema = this.schemas.get(constructor);
if (!schema) {
return { valid: true, errors: [] };
}
schema.rules.forEach((rules, property) => {
const value = instance[property];
rules.forEach(rule => {
if (!rule.validate(value, instance)) {
errors.push({
property,
value,
message: typeof rule.message === 'function'
? rule.message(property, value)
: rule.message
});
}
});
});
return {
valid: errors.length === 0,
errors
};
}
}
interface ValidationSchema {
rules: Map<string, ValidationRule[]>;
}
interface ValidationResult {
valid: boolean;
errors: ValidationError[];
}
interface ValidationError {
property: string;
value: any;
message: string;
}
// Validation decorators
function IsRequired(message?: string | ((prop: string) => string)) {
return function(target: any, property: string) {
Validator.addRule(target, property, {
validate: (value) => value !== null && value !== undefined && value !== '',
message: message || `${property} is required`
});
};
}
function IsEmail(message?: string) {
return function(target: any, property: string) {
Validator.addRule(target, property, {
validate: (value) => {
if (!value) return true; // Skip if empty
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
},
message: message || `${property} must be a valid email address`
});
};
}
function MinValue(min: number, message?: string) {
return function(target: any, property: string) {
Validator.addRule(target, property, {
validate: (value) => value == null || value >= min,
message: message || `${property} must be at least ${min}`
});
};
}
function MaxValue(max: number, message?: string) {
return function(target: any, property: string) {
Validator.addRule(target, property, {
validate: (value) => value == null || value <= max,
message: message || `${property} must be at most ${max}`
});
};
}
function Pattern(pattern: RegExp, message?: string) {
return function(target: any, property: string) {
Validator.addRule(target, property, {
validate: (value) => !value || pattern.test(value),
message: message || `${property} does not match required pattern`
});
};
}
function Custom(validator: (value: any, instance: any) => boolean, message: string) {
return function(target: any, property: string) {
Validator.addRule(target, property, {
validate: validator,
message
});
};
}
// Example usage
class RegistrationForm {
@IsRequired()
@MinLength(3)
@MaxLength(20)
@Pattern(/^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores')
username: string;
@IsRequired()
@IsEmail()
email: string;
@IsRequired()
@MinLength(8)
@Pattern(/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, 'Password must contain lowercase, uppercase, and number')
password: string;
@IsRequired()
@Custom((value, instance) => value === instance.password, 'Passwords must match')
confirmPassword: string;
@IsRequired()
@MinValue(18, 'You must be at least 18 years old')
@MaxValue(120, 'Please enter a valid age')
age: number;
@Custom((value) => value === true, 'You must accept the terms and conditions')
acceptTerms: boolean;
}
// Validate instance
const form = new RegistrationForm();
form.username = 'john_doe';
form.email = 'invalid-email';
form.password = 'weak';
form.confirmPassword = 'different';
form.age = 16;
form.acceptTerms = false;
const result = Validator.validate(form);
console.log('Valid:', result.valid);
console.log('Errors:', result.errors);
Best Practices
- Keep decorators simple and focused
// Good - single responsibility
@Log
@Validate
@Cache
method() { }
// Bad - doing too much
@LogValidateCacheAndRetry
method() { }
- Use decorator factories for configuration
// Good - configurable
@Retry({ attempts: 3, delay: 1000 })
@Cache({ ttl: 60000 })
method() { }
// Less flexible
@RetryThreeTimes
@CacheOneMinute
method() { }
- Preserve method signatures
// Good - preserves original signature
function Log(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Calling ${key}`);
return original.apply(this, args);
};
return descriptor;
}
- Handle errors gracefully
// Good - proper error handling
function SafeExecute(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = async function(...args: any[]) {
try {
return await original.apply(this, args);
} catch (error) {
console.error(`Error in ${key}:`, error);
// Handle or re-throw as appropriate
throw error;
}
};
return descriptor;
}
Conclusion
Decorators provide a powerful way to add behavior to classes and their members in a declarative manner. They enable cleaner code organization, better separation of concerns, and reusable cross-cutting functionality. While still a proposal for JavaScript, decorators are widely used in TypeScript and form the foundation of many modern frameworks. Understanding decorators is essential for advanced JavaScript development and building maintainable, scalable applications.