+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 52 of 354

๐ŸŽจ Decorators Introduction: Metadata and Reflection

Master TypeScript decorators to add metadata, modify behavior, and enable powerful metaprogramming patterns ๐Ÿš€

๐Ÿ’ŽAdvanced
30 min read

Prerequisites

  • Strong understanding of classes and functions ๐Ÿ“
  • Knowledge of higher-order functions ๐Ÿ”
  • Familiarity with metadata concepts ๐Ÿ’ป

What you'll learn

  • Understand decorator syntax and execution ๐ŸŽฏ
  • Enable experimental decorators in TypeScript ๐Ÿ—๏ธ
  • Work with metadata reflection API ๐Ÿ›ก๏ธ
  • Build foundation for advanced decorator patterns โœจ

๐ŸŽฏ Introduction

Welcome to the magical world of decorators! ๐ŸŽ‰ In this guide, weโ€™ll explore TypeScriptโ€™s decorator feature - a powerful way to modify classes, methods, and properties using a declarative syntax inspired by annotations in other languages.

Youโ€™ll discover how decorators are like enchantments ๐Ÿช„ - they add special powers to your code without changing its core structure! Whether youโ€™re building frameworks ๐Ÿ—๏ธ, implementing aspect-oriented programming ๐ŸŽฏ, or adding metadata for runtime reflection ๐Ÿ”, understanding decorators opens up metaprogramming possibilities.

By the end of this tutorial, youโ€™ll understand the decorator execution model, metadata reflection, and be ready to create your own decorators! Letโ€™s add some magic to our code! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Decorators

๐Ÿค” What are Decorators?

Decorators are special declarations that can be attached to classes, methods, properties, or parameters. They use the @expression syntax and are essentially functions that execute at runtime to modify or annotate the target theyโ€™re applied to.

Think of decorators like:

  • ๐Ÿช„ Magic spells: Enhancing objects with special abilities
  • ๐Ÿท๏ธ Labels: Adding metadata tags to code elements
  • ๐ŸŽ Gift wrapping: Adding layers of functionality
  • ๐Ÿ”ง Power tools: Modifying behavior without touching internals

๐Ÿ’ก Why Use Decorators?

Hereโ€™s why developers love decorators:

  1. Declarative Syntax ๐Ÿ“: Clear, readable way to add functionality
  2. Separation of Concerns ๐ŸŽฏ: Keep cross-cutting concerns separate
  3. Reusability โ™ป๏ธ: Apply the same enhancement multiple times
  4. Framework Building ๐Ÿ—๏ธ: Essential for modern frameworks

Real-world examples: Angularโ€™s @Component ๐Ÿ…ฐ๏ธ, NestJSโ€™s @Controller ๐ŸŽฎ, TypeORMโ€™s @Entity ๐Ÿ’พ - decorators are everywhere in modern TypeScript frameworks!

๐Ÿ”ง Setting Up Decorators

๐Ÿ“ Enabling Decorators

First, letโ€™s configure TypeScript for decorators:

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "strict": true,
    "moduleResolution": "node"
  }
}

๐ŸŽจ Basic Decorator Syntax

Letโ€™s explore the fundamental patterns:

// ๐ŸŽฏ Simple logging decorator
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  
  descriptor.value = function(...args: any[]) {
    console.log(`Calling ${propertyKey} with args:`, args);
    const result = originalMethod.apply(this, args);
    console.log(`${propertyKey} returned:`, result);
    return result;
  };
  
  return descriptor;
}

// ๐Ÿ  Using the decorator
class Calculator {
  @log
  add(a: number, b: number): number {
    return a + b;
  }
  
  @log
  multiply(a: number, b: number): number {
    return a * b;
  }
}

const calc = new Calculator();
calc.add(5, 3);
// Output:
// Calling add with args: [5, 3]
// add returned: 8

// ๐ŸŽฏ Decorator factories
function logWithPrefix(prefix: string) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    
    descriptor.value = function(...args: any[]) {
      console.log(`[${prefix}] Calling ${propertyKey}`);
      return originalMethod.apply(this, args);
    };
    
    return descriptor;
  };
}

class Service {
  @logWithPrefix('API')
  fetchData(): string {
    return 'data';
  }
  
  @logWithPrefix('DB')
  saveData(data: string): void {
    console.log('Saving:', data);
  }
}

๐Ÿš€ Decorator Types and Execution Order

๐Ÿ“Š Different Decorator Types

TypeScript supports five types of decorators:

// ๐Ÿ—๏ธ Class decorator
function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
  console.log(`Sealed class: ${constructor.name}`);
}

// ๐Ÿ”ง Method decorator
function enumerable(value: boolean) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.enumerable = value;
    console.log(`Made ${propertyKey} enumerable: ${value}`);
  };
}

// ๐Ÿ“ฆ Property decorator
function readonly(target: any, propertyKey: string) {
  console.log(`Making ${propertyKey} readonly`);
  
  let value = target[propertyKey];
  
  const getter = () => value;
  const setter = (newVal: any) => {
    console.log(`Cannot set readonly property ${propertyKey}`);
  };
  
  Object.defineProperty(target, propertyKey, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true
  });
}

// ๐ŸŽฏ Parameter decorator
function required(target: any, propertyKey: string, parameterIndex: number) {
  console.log(`Parameter ${parameterIndex} of ${propertyKey} is required`);
  
  // Store metadata about required parameters
  const existingRequiredParameters = Reflect.getOwnMetadata('required', target, propertyKey) || [];
  existingRequiredParameters.push(parameterIndex);
  Reflect.defineMetadata('required', existingRequiredParameters, target, propertyKey);
}

// ๐Ÿ” Accessor decorator
function configurable(value: boolean) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.configurable = value;
    console.log(`Made ${propertyKey} configurable: ${value}`);
  };
}

// ๐Ÿ’ซ Using all decorator types
@sealed
class User {
  @readonly
  id: string = 'user_001';
  
  private _name: string = '';
  
  @configurable(false)
  get name(): string {
    return this._name;
  }
  
  set name(value: string) {
    this._name = value;
  }
  
  @enumerable(false)
  updateProfile(
    @required name: string,
    @required email: string,
    bio?: string
  ): void {
    console.log('Updating profile...');
  }
}

// ๐ŸŽฏ Execution order demonstration
function first() {
  console.log('first(): factory evaluated');
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log('first(): called');
  };
}

function second() {
  console.log('second(): factory evaluated');
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log('second(): called');
  };
}

class ExampleClass {
  @first()
  @second()
  method() {}
}

// Output:
// first(): factory evaluated
// second(): factory evaluated
// second(): called
// first(): called

๐Ÿ” Metadata Reflection API

๐Ÿ“Š Working with Metadata

Using the Reflect Metadata API for powerful runtime introspection:

// First, install: npm install reflect-metadata
import 'reflect-metadata';

// ๐ŸŽฏ Custom metadata decorators
const MetadataKeys = {
  ROUTE: 'route',
  METHOD: 'method',
  MIDDLEWARE: 'middleware',
  ROLES: 'roles',
  VALIDATION: 'validation'
} as const;

// ๐ŸŒ Route decorator
function Route(path: string) {
  return function(target: any, propertyKey: string) {
    Reflect.defineMetadata(MetadataKeys.ROUTE, path, target, propertyKey);
  };
}

// ๐Ÿ”ง HTTP method decorators
function Get(path: string) {
  return function(target: any, propertyKey: string) {
    Reflect.defineMetadata(MetadataKeys.ROUTE, path, target, propertyKey);
    Reflect.defineMetadata(MetadataKeys.METHOD, 'GET', target, propertyKey);
  };
}

function Post(path: string) {
  return function(target: any, propertyKey: string) {
    Reflect.defineMetadata(MetadataKeys.ROUTE, path, target, propertyKey);
    Reflect.defineMetadata(MetadataKeys.METHOD, 'POST', target, propertyKey);
  };
}

// ๐Ÿ›ก๏ธ Middleware decorator
function UseMiddleware(...middleware: Function[]) {
  return function(target: any, propertyKey?: string) {
    if (propertyKey) {
      // Method decorator
      Reflect.defineMetadata(MetadataKeys.MIDDLEWARE, middleware, target, propertyKey);
    } else {
      // Class decorator
      Reflect.defineMetadata(MetadataKeys.MIDDLEWARE, middleware, target);
    }
  };
}

// ๐Ÿ” Role-based access decorator
function RequireRoles(...roles: string[]) {
  return function(target: any, propertyKey: string) {
    Reflect.defineMetadata(MetadataKeys.ROLES, roles, target, propertyKey);
  };
}

// ๐Ÿ“ Validation decorator
interface ValidationRule {
  validator: (value: any) => boolean;
  message: string;
}

function Validate(rules: ValidationRule[]) {
  return function(target: any, propertyKey: string) {
    Reflect.defineMetadata(MetadataKeys.VALIDATION, rules, target, propertyKey);
  };
}

// ๐Ÿ—๏ธ Controller using metadata
@UseMiddleware(authMiddleware, loggingMiddleware)
class UserController {
  @Get('/users')
  @RequireRoles('admin', 'user')
  getAllUsers() {
    return { users: [] };
  }
  
  @Post('/users')
  @RequireRoles('admin')
  @UseMiddleware(validationMiddleware)
  @Validate([
    { validator: (data) => data.email?.includes('@'), message: 'Invalid email' },
    { validator: (data) => data.password?.length >= 8, message: 'Password too short' }
  ])
  createUser(data: any) {
    return { id: '123', ...data };
  }
  
  @Get('/users/:id')
  @RequireRoles('admin', 'user')
  getUser(id: string) {
    return { id, name: 'John Doe' };
  }
}

// ๐Ÿ” Reading metadata
function analyzeController(controller: any) {
  const className = controller.constructor.name;
  console.log(`\nAnalyzing ${className}:`);
  
  // Class-level middleware
  const classMiddleware = Reflect.getMetadata(MetadataKeys.MIDDLEWARE, controller.constructor) || [];
  console.log(`Class middleware: ${classMiddleware.map(m => m.name).join(', ')}`);
  
  // Analyze methods
  const prototype = controller.constructor.prototype;
  const methodNames = Object.getOwnPropertyNames(prototype)
    .filter(name => name !== 'constructor');
  
  methodNames.forEach(methodName => {
    const route = Reflect.getMetadata(MetadataKeys.ROUTE, prototype, methodName);
    const method = Reflect.getMetadata(MetadataKeys.METHOD, prototype, methodName);
    const roles = Reflect.getMetadata(MetadataKeys.ROLES, prototype, methodName);
    const middleware = Reflect.getMetadata(MetadataKeys.MIDDLEWARE, prototype, methodName);
    const validation = Reflect.getMetadata(MetadataKeys.VALIDATION, prototype, methodName);
    
    if (route) {
      console.log(`\n  ${methodName}():`);
      console.log(`    Route: ${method || 'GET'} ${route}`);
      if (roles) console.log(`    Roles: ${roles.join(', ')}`);
      if (middleware) console.log(`    Middleware: ${middleware.map(m => m.name).join(', ')}`);
      if (validation) console.log(`    Validation rules: ${validation.length}`);
    }
  });
}

// Mock middleware functions
function authMiddleware() {}
function loggingMiddleware() {}
function validationMiddleware() {}

const controller = new UserController();
analyzeController(controller);

๐ŸŽญ Advanced Decorator Patterns

๐Ÿ”„ Decorator Composition

Building complex behaviors through composition:

// ๐ŸŽฏ Composable decorators
function compose(...decorators: MethodDecorator[]): MethodDecorator {
  return function(target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
    decorators.forEach(decorator => decorator(target, propertyKey, descriptor));
    return descriptor;
  };
}

// โฑ๏ธ Performance monitoring decorator
function measureTime(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 end = performance.now();
      console.log(`โฑ๏ธ ${propertyKey} took ${(end - start).toFixed(2)}ms`);
      return result;
    } catch (error) {
      const end = performance.now();
      console.log(`โฑ๏ธ ${propertyKey} failed after ${(end - start).toFixed(2)}ms`);
      throw error;
    }
  };
}

// ๐Ÿ’พ Caching decorator
function cache(ttl: number = 60000) {
  const cacheMap = new Map<string, { value: any; timestamp: 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 = cacheMap.get(key);
      
      if (cached && Date.now() - cached.timestamp < ttl) {
        console.log(`๐Ÿ’พ Cache hit for ${propertyKey}`);
        return cached.value;
      }
      
      const result = originalMethod.apply(this, args);
      cacheMap.set(key, { value: result, timestamp: Date.now() });
      console.log(`๐Ÿ’พ Cached result for ${propertyKey}`);
      return result;
    };
  };
}

// ๐Ÿ”„ Retry decorator
function retry(attempts: number = 3, delay: number = 1000) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    
    descriptor.value = async function(...args: any[]) {
      for (let i = 0; i < attempts; i++) {
        try {
          return await originalMethod.apply(this, args);
        } catch (error) {
          console.log(`๐Ÿ”„ Attempt ${i + 1} failed for ${propertyKey}`);
          if (i === attempts - 1) throw error;
          await new Promise(resolve => setTimeout(resolve, delay));
        }
      }
    };
  };
}

// ๐Ÿ›ก๏ธ Error handling decorator
function handleErrors(handler?: (error: Error) => any) {
  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) {
        console.error(`โŒ Error in ${propertyKey}:`, error);
        if (handler) {
          return handler(error as Error);
        }
        throw error;
      }
    };
  };
}

// ๐Ÿ—๏ธ Service using composed decorators
class DataService {
  @compose(
    measureTime,
    handleErrors(() => ({ error: 'Service unavailable' })),
    retry(3, 500),
    cache(5000)
  )
  async fetchData(id: string): Promise<any> {
    // Simulate API call
    if (Math.random() > 0.7) {
      throw new Error('Network error');
    }
    
    await new Promise(resolve => setTimeout(resolve, 100));
    return { id, data: `Data for ${id}` };
  }
  
  @measureTime
  @cache(10000)
  processData(data: any): any {
    // Simulate expensive computation
    const start = Date.now();
    while (Date.now() - start < 50) {} // Busy wait
    
    return { ...data, processed: true };
  }
}

// ๐Ÿ’ซ Usage
const service = new DataService();

async function testService() {
  // First call - will fetch
  await service.fetchData('123');
  
  // Second call - cache hit
  await service.fetchData('123');
  
  // Process data
  service.processData({ value: 42 });
  service.processData({ value: 42 }); // Cache hit
}

๐Ÿ—๏ธ Property Injection Pattern

Implementing dependency injection with decorators:

// ๐ŸŽฏ Dependency injection system
const container = new Map<string, any>();

function Injectable(token?: string) {
  return function(target: any) {
    const t = token || target.name;
    container.set(t, target);
    console.log(`๐Ÿ’‰ Registered ${t} in container`);
  };
}

function Inject(token: string) {
  return function(target: any, propertyKey: string) {
    Object.defineProperty(target, propertyKey, {
      get() {
        const service = container.get(token);
        if (!service) {
          throw new Error(`Service ${token} not found in container`);
        }
        return new service();
      },
      enumerable: true,
      configurable: true
    });
  };
}

// ๐Ÿ”ง Auto-wire dependencies
function AutoWired(target: any, propertyKey: string) {
  const type = Reflect.getMetadata('design:type', target, propertyKey);
  
  Object.defineProperty(target, propertyKey, {
    get() {
      const service = container.get(type.name);
      if (!service) {
        throw new Error(`Service ${type.name} not found for auto-wiring`);
      }
      return new service();
    },
    enumerable: true,
    configurable: true
  });
}

// ๐Ÿ“Š Services with dependency injection
@Injectable()
class LoggerService {
  log(message: string): void {
    console.log(`[LOG] ${new Date().toISOString()}: ${message}`);
  }
}

@Injectable()
class DatabaseService {
  @AutoWired
  private logger!: LoggerService;
  
  query(sql: string): any {
    this.logger.log(`Executing query: ${sql}`);
    return { rows: [] };
  }
}

@Injectable('EmailService')
class EmailService {
  @AutoWired
  private logger!: LoggerService;
  
  send(to: string, subject: string, body: string): void {
    this.logger.log(`Sending email to ${to}: ${subject}`);
  }
}

// ๐Ÿ  Main application class
class Application {
  @AutoWired
  private database!: DatabaseService;
  
  @Inject('EmailService')
  private email!: EmailService;
  
  @AutoWired
  private logger!: LoggerService;
  
  run(): void {
    this.logger.log('Application starting...');
    
    const users = this.database.query('SELECT * FROM users');
    this.email.send('[email protected]', 'App Started', 'The application has started');
    
    this.logger.log('Application ready!');
  }
}

// ๐Ÿ’ซ Usage
const app = new Application();
app.run();

๐ŸŽจ Building a Mini Framework

๐Ÿ—๏ธ Creating a Web Framework with Decorators

Letโ€™s build a simple Express-like framework:

// ๐ŸŒ Mini web framework
interface Route {
  method: string;
  path: string;
  handler: Function;
  middleware: Function[];
}

class Router {
  private routes: Route[] = [];
  
  addRoute(method: string, path: string, handler: Function, middleware: Function[] = []): void {
    this.routes.push({ method, path, handler, middleware });
  }
  
  getRoutes(): Route[] {
    return this.routes;
  }
}

const globalRouter = new Router();

// ๐ŸŽฏ Controller decorator
function Controller(basePath: string = '') {
  return function(target: any) {
    const router = new Router();
    const prototype = target.prototype;
    
    // Collect all routes from methods
    Object.getOwnPropertyNames(prototype).forEach(methodName => {
      if (methodName === 'constructor') return;
      
      const routePath = Reflect.getMetadata('path', prototype, methodName);
      const httpMethod = Reflect.getMetadata('method', prototype, methodName);
      const middleware = Reflect.getMetadata('middleware', prototype, methodName) || [];
      
      if (routePath && httpMethod) {
        const fullPath = basePath + routePath;
        const handler = prototype[methodName].bind(prototype);
        
        router.addRoute(httpMethod, fullPath, handler, middleware);
        globalRouter.addRoute(httpMethod, fullPath, handler, middleware);
      }
    });
    
    // Store router on the class
    Reflect.defineMetadata('router', router, target);
  };
}

// ๐Ÿ”ง HTTP method decorators
function createMethodDecorator(method: string) {
  return function(path: string) {
    return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
      Reflect.defineMetadata('path', path, target, propertyKey);
      Reflect.defineMetadata('method', method, target, propertyKey);
    };
  };
}

const Get = createMethodDecorator('GET');
const Post = createMethodDecorator('POST');
const Put = createMethodDecorator('PUT');
const Delete = createMethodDecorator('DELETE');
const Patch = createMethodDecorator('PATCH');

// ๐Ÿ›ก๏ธ Middleware decorator
function Use(...middleware: Function[]) {
  return function(target: any, propertyKey?: string) {
    if (propertyKey) {
      const existing = Reflect.getMetadata('middleware', target, propertyKey) || [];
      Reflect.defineMetadata('middleware', [...existing, ...middleware], target, propertyKey);
    } else {
      // Class-level middleware
      const existing = Reflect.getMetadata('middleware', target) || [];
      Reflect.defineMetadata('middleware', [...existing, ...middleware], target);
    }
  };
}

// ๐Ÿ” Auth decorators
function RequireAuth(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const middleware = Reflect.getMetadata('middleware', target, propertyKey) || [];
  middleware.push(authMiddleware);
  Reflect.defineMetadata('middleware', middleware, target, propertyKey);
}

function RequireRole(role: string) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const middleware = Reflect.getMetadata('middleware', target, propertyKey) || [];
    middleware.push((req: any) => checkRole(req, role));
    Reflect.defineMetadata('middleware', middleware, target, propertyKey);
  };
}

// ๐Ÿ“ Request/Response types
interface Request {
  params: Record<string, string>;
  query: Record<string, string>;
  body: any;
  headers: Record<string, string>;
  user?: any;
}

interface Response {
  status(code: number): Response;
  json(data: any): void;
  send(data: string): void;
}

// ๐Ÿ  Example controllers
@Controller('/api/users')
@Use(loggingMiddleware)
class UserController {
  @Get('/')
  getAllUsers(req: Request, res: Response): void {
    res.json({ users: ['Alice', 'Bob', 'Charlie'] });
  }
  
  @Get('/:id')
  @RequireAuth
  getUser(req: Request, res: Response): void {
    const { id } = req.params;
    res.json({ id, name: 'John Doe' });
  }
  
  @Post('/')
  @RequireRole('admin')
  @Use(validationMiddleware)
  createUser(req: Request, res: Response): void {
    const user = req.body;
    res.status(201).json({ id: '123', ...user });
  }
  
  @Put('/:id')
  @RequireAuth
  updateUser(req: Request, res: Response): void {
    const { id } = req.params;
    res.json({ id, ...req.body, updated: true });
  }
  
  @Delete('/:id')
  @RequireRole('admin')
  deleteUser(req: Request, res: Response): void {
    const { id } = req.params;
    res.json({ deleted: true, id });
  }
}

@Controller('/api/posts')
class PostController {
  @Get('/')
  getPosts(req: Request, res: Response): void {
    res.json({ posts: [] });
  }
  
  @Post('/')
  @RequireAuth
  createPost(req: Request, res: Response): void {
    res.status(201).json({ id: '456', ...req.body });
  }
}

// ๐Ÿš€ Framework initialization
function createApp() {
  const routes = globalRouter.getRoutes();
  
  console.log('\n๐Ÿš€ Registered Routes:');
  routes.forEach(route => {
    const middlewareNames = route.middleware.map(m => m.name).join(', ');
    console.log(`  ${route.method} ${route.path} [${middlewareNames}]`);
  });
  
  return {
    routes,
    listen(port: number) {
      console.log(`\n๐ŸŒ Server listening on port ${port}`);
    }
  };
}

// Mock middleware
function loggingMiddleware(req: Request) {
  console.log(`๐Ÿ“ ${req.method} ${req.path}`);
}

function authMiddleware(req: Request) {
  if (!req.headers.authorization) {
    throw new Error('Unauthorized');
  }
}

function validationMiddleware(req: Request) {
  // Validate request body
}

function checkRole(req: Request, role: string) {
  if (req.user?.role !== role) {
    throw new Error(`Requires role: ${role}`);
  }
}

// ๐Ÿ’ซ Initialize app
const app = createApp();
app.listen(3000);

๐ŸŽฎ Hands-On Exercise

Letโ€™s build a validation system using decorators!

๐Ÿ“ Challenge: Type-Safe Validation System

Create a validation system that:

  1. Uses decorators to define validation rules
  2. Supports custom validators
  3. Collects and runs validations
  4. Provides detailed error messages
// Your challenge: Implement this validation system
interface ValidationResult {
  valid: boolean;
  errors: Array<{ property: string; message: string }>;
}

// Decorators to implement:
// @IsRequired - Property must have a value
// @MinLength(n) - String must be at least n characters
// @MaxLength(n) - String must be at most n characters
// @IsEmail - Must be valid email format
// @Min(n) - Number must be >= n
// @Max(n) - Number must be <= n
// @Pattern(regex) - Must match regex pattern
// @Custom(validator) - Custom validation function

// Example usage to support:
class UserRegistration {
  @IsRequired
  @MinLength(3)
  @MaxLength(20)
  username!: string;
  
  @IsRequired
  @IsEmail
  email!: string;
  
  @IsRequired
  @MinLength(8)
  @Pattern(/^(?=.*[A-Za-z])(?=.*\d)/)
  password!: string;
  
  @Min(18)
  @Max(120)
  age?: number;
  
  @Custom((value) => value !== 'admin')
  role?: string;
}

// Implement the validation function
function validate(instance: any): ValidationResult {
  // Your implementation
}

๐Ÿ’ก Solution

Click to see the solution
import 'reflect-metadata';

// ๐ŸŽฏ Validation metadata key
const VALIDATION_KEY = Symbol('validations');

// ๐Ÿ“Š Validation rule interface
interface ValidationRule {
  validator: (value: any, instance?: any) => boolean;
  message: (propertyName: string, value: any) => string;
}

// ๐Ÿ—๏ธ Base validation decorator factory
function addValidation(rule: ValidationRule) {
  return function(target: any, propertyKey: string) {
    const existingRules = Reflect.getMetadata(VALIDATION_KEY, target, propertyKey) || [];
    existingRules.push(rule);
    Reflect.defineMetadata(VALIDATION_KEY, existingRules, target, propertyKey);
  };
}

// โœ… Required validator
function IsRequired(target: any, propertyKey: string) {
  addValidation({
    validator: (value) => value !== undefined && value !== null && value !== '',
    message: (prop) => `${prop} is required`
  })(target, propertyKey);
}

// ๐Ÿ“ Length validators
function MinLength(min: number) {
  return addValidation({
    validator: (value) => !value || value.length >= min,
    message: (prop, value) => `${prop} must be at least ${min} characters (got ${value?.length || 0})`
  });
}

function MaxLength(max: number) {
  return addValidation({
    validator: (value) => !value || value.length <= max,
    message: (prop, value) => `${prop} must be at most ${max} characters (got ${value?.length || 0})`
  });
}

// ๐Ÿ“ง Email validator
function IsEmail(target: any, propertyKey: string) {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  addValidation({
    validator: (value) => !value || emailRegex.test(value),
    message: (prop, value) => `${prop} must be a valid email address (got "${value}")`
  })(target, propertyKey);
}

// ๐Ÿ”ข Number validators
function Min(min: number) {
  return addValidation({
    validator: (value) => value === undefined || value >= min,
    message: (prop, value) => `${prop} must be at least ${min} (got ${value})`
  });
}

function Max(max: number) {
  return addValidation({
    validator: (value) => value === undefined || value <= max,
    message: (prop, value) => `${prop} must be at most ${max} (got ${value})`
  });
}

// ๐ŸŽฏ Pattern validator
function Pattern(pattern: RegExp, message?: string) {
  return addValidation({
    validator: (value) => !value || pattern.test(value),
    message: (prop, value) => message || `${prop} must match pattern ${pattern} (got "${value}")`
  });
}

// ๐Ÿ”ง Custom validator
function Custom(
  validator: (value: any, instance?: any) => boolean,
  message?: string | ((prop: string, value: any) => string)
) {
  return addValidation({
    validator,
    message: typeof message === 'function' 
      ? message 
      : (prop, value) => message || `${prop} failed custom validation`
  });
}

// ๐ŸŽจ Composite validators
function IsAlphanumeric(target: any, propertyKey: string) {
  Pattern(/^[a-zA-Z0-9]+$/, `${propertyKey} must contain only letters and numbers`)(target, propertyKey);
}

function IsUrl(target: any, propertyKey: string) {
  const urlRegex = /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/;
  Pattern(urlRegex, `${propertyKey} must be a valid URL`)(target, propertyKey);
}

// ๐Ÿ—๏ธ Validation result interface
interface ValidationResult {
  valid: boolean;
  errors: Array<{ property: string; message: string }>;
}

// ๐Ÿ” Validation function
function validate(instance: any): ValidationResult {
  const errors: Array<{ property: string; message: string }> = [];
  const prototype = Object.getPrototypeOf(instance);
  
  // Get all properties
  const properties = [
    ...Object.getOwnPropertyNames(instance),
    ...Object.getOwnPropertyNames(prototype)
  ].filter(prop => prop !== 'constructor');
  
  // Validate each property
  for (const propertyKey of properties) {
    const rules = Reflect.getMetadata(VALIDATION_KEY, prototype, propertyKey) || [];
    const value = instance[propertyKey];
    
    for (const rule of rules) {
      if (!rule.validator(value, instance)) {
        errors.push({
          property: propertyKey,
          message: rule.message(propertyKey, value)
        });
      }
    }
  }
  
  return {
    valid: errors.length === 0,
    errors
  };
}

// ๐ŸŽฏ Advanced validation decorators
function ValidateNested(target: any, propertyKey: string) {
  addValidation({
    validator: (value) => {
      if (!value) return true;
      const result = validate(value);
      return result.valid;
    },
    message: (prop, value) => {
      if (!value) return `${prop} is invalid`;
      const result = validate(value);
      return `${prop} has validation errors: ${result.errors.map(e => e.message).join(', ')}`;
    }
  })(target, propertyKey);
}

function ArrayMinSize(min: number) {
  return addValidation({
    validator: (value) => !value || (Array.isArray(value) && value.length >= min),
    message: (prop, value) => `${prop} must have at least ${min} items (got ${value?.length || 0})`
  });
}

function ArrayMaxSize(max: number) {
  return addValidation({
    validator: (value) => !value || (Array.isArray(value) && value.length <= max),
    message: (prop, value) => `${prop} must have at most ${max} items (got ${value?.length || 0})`
  });
}

// ๐Ÿ’ซ Example usage
class Address {
  @IsRequired
  street!: string;
  
  @IsRequired
  city!: string;
  
  @Pattern(/^\d{5}$/, 'Zip code must be 5 digits')
  zipCode!: string;
}

class UserRegistration {
  @IsRequired
  @MinLength(3)
  @MaxLength(20)
  @IsAlphanumeric
  username!: string;
  
  @IsRequired
  @IsEmail
  email!: string;
  
  @IsRequired
  @MinLength(8)
  @Pattern(
    /^(?=.*[A-Za-z])(?=.*\d)/,
    'Password must contain at least one letter and one number'
  )
  password!: string;
  
  @Min(18)
  @Max(120)
  age?: number;
  
  @Custom(
    (value) => !value || value !== 'admin',
    'Role cannot be "admin"'
  )
  role?: string;
  
  @IsUrl
  website?: string;
  
  @ValidateNested
  address?: Address;
  
  @ArrayMinSize(1)
  @ArrayMaxSize(5)
  @Custom(
    (value) => !value || value.every((tag: string) => tag.length > 0),
    'All tags must be non-empty'
  )
  tags?: string[];
}

// ๐Ÿงช Test the validation system
console.log('=== Validation System Demo ===\n');

// Test 1: Invalid registration
const user1 = new UserRegistration();
user1.username = 'ab';  // Too short
user1.email = 'invalid-email';
user1.password = 'weak';
user1.age = 200;
user1.role = 'admin';
user1.website = 'not-a-url';
user1.tags = ['', 'tag2'];

console.log('Test 1 - Invalid data:');
const result1 = validate(user1);
console.log(`Valid: ${result1.valid}`);
result1.errors.forEach(error => {
  console.log(`  โŒ ${error.message}`);
});

// Test 2: Valid registration
const user2 = new UserRegistration();
user2.username = 'john123';
user2.email = '[email protected]';
user2.password = 'SecurePass123';
user2.age = 25;
user2.role = 'user';
user2.website = 'https://example.com';
user2.address = new Address();
user2.address.street = '123 Main St';
user2.address.city = 'New York';
user2.address.zipCode = '12345';
user2.tags = ['coding', 'typescript'];

console.log('\nTest 2 - Valid data:');
const result2 = validate(user2);
console.log(`Valid: ${result2.valid}`);
if (result2.errors.length > 0) {
  result2.errors.forEach(error => {
    console.log(`  โŒ ${error.message}`);
  });
} else {
  console.log('  โœ… All validations passed!');
}

// ๐Ÿ”ง Validation helper function
function assertValid<T>(instance: T): asserts instance is T {
  const result = validate(instance);
  if (!result.valid) {
    const errorMessages = result.errors.map(e => `  - ${e.message}`).join('\n');
    throw new Error(`Validation failed:\n${errorMessages}`);
  }
}

// Usage with type narrowing
try {
  assertValid(user2);
  console.log('\nโœ… User registration is valid and type-safe!');
} catch (error) {
  console.error('โŒ Validation error:', error.message);
}

๐ŸŽฏ Summary

Youโ€™ve mastered the fundamentals of TypeScript decorators! ๐ŸŽ‰ You learned how to:

  • ๐ŸŽจ Enable and use decorators in TypeScript
  • ๐Ÿ”ง Create different types of decorators (class, method, property, parameter)
  • ๐Ÿ“Š Work with metadata reflection API
  • ๐Ÿ—๏ธ Build decorator factories and compose decorators
  • ๐Ÿ” Implement dependency injection patterns
  • โœจ Create mini-frameworks using decorators

Decorators provide a powerful way to add metadata and modify behavior declaratively, making them essential for building modern TypeScript applications and frameworks!

Keep exploring the power of metaprogramming with decorators! ๐Ÿš€