+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 55 of 354

๐ŸŽญ Parameter Decorators: Function Parameter Enhancement

Master parameter decorators in TypeScript to add metadata, validation, and dependency injection to function parameters ๐Ÿš€

๐Ÿ’ŽAdvanced
25 min read

Prerequisites

  • Understanding of decorators basics ๐Ÿ“
  • Knowledge of reflection metadata ๐Ÿ”
  • Familiarity with dependency injection concepts ๐Ÿ’ป

What you'll learn

  • Create parameter decorators for metadata ๐ŸŽฏ
  • Implement parameter validation systems ๐Ÿ—๏ธ
  • Build dependency injection with parameters ๐Ÿ›ก๏ธ
  • Master advanced parameter patterns โœจ

๐ŸŽฏ Introduction

Welcome to the specialized world of parameter decorators! ๐ŸŽ‰ In this guide, weโ€™ll explore how to use decorators on function parameters to add metadata, enable validation, and implement sophisticated dependency injection patterns.

Youโ€™ll discover how parameter decorators are like parameter tags ๐Ÿท๏ธ - they mark specific parameters with metadata that can be used by other decorators or frameworks! Whether youโ€™re building validation systems ๐Ÿ“Š, implementing dependency injection ๐Ÿ’‰, or creating API documentation ๐Ÿ“, parameter decorators provide the foundation.

By the end of this tutorial, youโ€™ll be confidently using parameter decorators to enhance your function parameters with powerful capabilities! Letโ€™s dive into parameter enhancement! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Parameter Decorators

๐Ÿค” How Parameter Decorators Work

Parameter decorators are unique among decorator types:

  • They can only add metadata, not modify behavior directly
  • They work in conjunction with method decorators
  • They execute before method decorators
  • They receive the parameter index as an argument

Think of parameter decorators like:

  • ๐Ÿท๏ธ Name tags: Identifying parameters with metadata
  • ๐Ÿ“ Bookmarks: Marking important parameters
  • ๐Ÿ—บ๏ธ Maps: Guiding other decorators to specific parameters
  • ๐Ÿ“‹ Annotations: Adding notes about parameter requirements

๐Ÿ’ก Parameter Decorator Signature

type ParameterDecorator = (
  target: any,                    // The prototype of the class
  propertyKey: string | symbol,   // The method name
  parameterIndex: number         // The index of the parameter
) => void;

๐Ÿ”ง Basic Parameter Decorators

๐Ÿ“ Metadata Collection

Letโ€™s start with fundamental parameter decorator patterns:

import 'reflect-metadata';

// ๐ŸŽฏ Basic parameter marking
const PARAM_TYPES_KEY = Symbol('paramTypes');
const PARAM_NAMES_KEY = Symbol('paramNames');

function logParameter(target: any, propertyKey: string, parameterIndex: number) {
  console.log(`Parameter decorator on ${propertyKey} at index ${parameterIndex}`);
}

// ๐Ÿท๏ธ Named parameter decorator
function Named(name: string) {
  return function(target: any, propertyKey: string, parameterIndex: number) {
    const existingNames = Reflect.getOwnMetadata(PARAM_NAMES_KEY, target, propertyKey) || {};
    existingNames[parameterIndex] = name;
    Reflect.defineMetadata(PARAM_NAMES_KEY, existingNames, target, propertyKey);
  };
}

// ๐Ÿ“ Type marking decorator
function Type(type: string) {
  return function(target: any, propertyKey: string, parameterIndex: number) {
    const existingTypes = Reflect.getOwnMetadata(PARAM_TYPES_KEY, target, propertyKey) || {};
    existingTypes[parameterIndex] = type;
    Reflect.defineMetadata(PARAM_TYPES_KEY, existingTypes, target, propertyKey);
  };
}

// ๐Ÿ” Parameter info collector
function collectParameterInfo(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  
  descriptor.value = function(...args: any[]) {
    const names = Reflect.getOwnMetadata(PARAM_NAMES_KEY, target, propertyKey) || {};
    const types = Reflect.getOwnMetadata(PARAM_TYPES_KEY, target, propertyKey) || {};
    
    console.log(`๐Ÿ“‹ Calling ${propertyKey} with parameters:`);
    args.forEach((arg, index) => {
      const name = names[index] || `param${index}`;
      const type = types[index] || 'unknown';
      console.log(`  - ${name} (${type}): ${JSON.stringify(arg)}`);
    });
    
    return originalMethod.apply(this, args);
  };
  
  return descriptor;
}

// ๐Ÿ  Example usage
class UserService {
  @collectParameterInfo
  createUser(
    @Named('username') @Type('string') username: string,
    @Named('email') @Type('email') email: string,
    @Named('age') @Type('number') age: number
  ): void {
    console.log('Creating user...');
  }
  
  @collectParameterInfo
  updateUser(
    @Named('userId') @Type('uuid') id: string,
    @Named('updates') @Type('object') updates: any
  ): void {
    console.log('Updating user...');
  }
}

// ๐Ÿ’ซ Testing
const service = new UserService();
service.createUser('johndoe', '[email protected]', 25);
service.updateUser('123e4567-e89b-12d3-a456-426614174000', { name: 'John Updated' });

๐Ÿ” Parameter Validation

Building validation with parameter decorators:

// ๐ŸŽฏ Validation metadata
const VALIDATORS_KEY = Symbol('validators');
const REQUIRED_KEY = Symbol('required');

interface Validator {
  validate: (value: any) => boolean;
  message: string;
}

// โœ… Required parameter
function Required(target: any, propertyKey: string, parameterIndex: number) {
  const existingRequired = Reflect.getOwnMetadata(REQUIRED_KEY, target, propertyKey) || [];
  existingRequired.push(parameterIndex);
  Reflect.defineMetadata(REQUIRED_KEY, existingRequired, target, propertyKey);
}

// ๐Ÿ“ Range validation
function Range(min: number, max: number) {
  return function(target: any, propertyKey: string, parameterIndex: number) {
    const existingValidators: Map<number, Validator[]> = 
      Reflect.getOwnMetadata(VALIDATORS_KEY, target, propertyKey) || new Map();
    
    const validators = existingValidators.get(parameterIndex) || [];
    validators.push({
      validate: (value) => value >= min && value <= max,
      message: `Parameter at index ${parameterIndex} must be between ${min} and ${max}`
    });
    
    existingValidators.set(parameterIndex, validators);
    Reflect.defineMetadata(VALIDATORS_KEY, existingValidators, target, propertyKey);
  };
}

// ๐Ÿ“ง Email validation
function Email(target: any, propertyKey: string, parameterIndex: number) {
  const existingValidators: Map<number, Validator[]> = 
    Reflect.getOwnMetadata(VALIDATORS_KEY, target, propertyKey) || new Map();
  
  const validators = existingValidators.get(parameterIndex) || [];
  validators.push({
    validate: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
    message: `Parameter at index ${parameterIndex} must be a valid email`
  });
  
  existingValidators.set(parameterIndex, validators);
  Reflect.defineMetadata(VALIDATORS_KEY, existingValidators, target, propertyKey);
}

// ๐ŸŽฏ Pattern validation
function Pattern(pattern: RegExp, message?: string) {
  return function(target: any, propertyKey: string, parameterIndex: number) {
    const existingValidators: Map<number, Validator[]> = 
      Reflect.getOwnMetadata(VALIDATORS_KEY, target, propertyKey) || new Map();
    
    const validators = existingValidators.get(parameterIndex) || [];
    validators.push({
      validate: (value) => pattern.test(value),
      message: message || `Parameter at index ${parameterIndex} must match pattern ${pattern}`
    });
    
    existingValidators.set(parameterIndex, validators);
    Reflect.defineMetadata(VALIDATORS_KEY, existingValidators, target, propertyKey);
  };
}

// ๐Ÿ›ก๏ธ Validation method decorator
function Validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  
  descriptor.value = function(...args: any[]) {
    // Check required parameters
    const required = Reflect.getOwnMetadata(REQUIRED_KEY, target, propertyKey) || [];
    for (const index of required) {
      if (args[index] === undefined || args[index] === null) {
        throw new Error(`${propertyKey}: Parameter at index ${index} is required`);
      }
    }
    
    // Run validators
    const validators: Map<number, Validator[]> = 
      Reflect.getOwnMetadata(VALIDATORS_KEY, target, propertyKey) || new Map();
    
    validators.forEach((paramValidators, index) => {
      const value = args[index];
      
      for (const validator of paramValidators) {
        if (!validator.validate(value)) {
          throw new Error(`${propertyKey}: ${validator.message} (got ${JSON.stringify(value)})`);
        }
      }
    });
    
    return originalMethod.apply(this, args);
  };
  
  return descriptor;
}

// ๐Ÿ  Example with validation
class PaymentService {
  @Validate
  processPayment(
    @Required @Range(0.01, 10000) amount: number,
    @Required @Pattern(/^[A-Z]{3}$/, 'Currency must be 3 uppercase letters') currency: string,
    @Email recipient: string
  ): void {
    console.log(`๐Ÿ’ฐ Processing ${currency} ${amount} to ${recipient}`);
  }
  
  @Validate
  createInvoice(
    @Required customerId: string,
    @Range(1, 100) items: number,
    @Pattern(/^INV-\d{6}$/) invoiceNumber?: string
  ): void {
    console.log(`๐Ÿ“„ Creating invoice ${invoiceNumber || 'AUTO'} for customer ${customerId}`);
  }
}

// ๐Ÿ’ซ Testing validation
const payment = new PaymentService();

// Valid calls
payment.processPayment(100, 'USD', '[email protected]');
payment.createInvoice('CUST-001', 5);

// Invalid calls
try {
  payment.processPayment(0, 'USD', '[email protected]'); // Too small
} catch (e) {
  console.error('โŒ', e.message);
}

try {
  payment.processPayment(100, 'usd', 'invalid-email'); // Invalid currency and email
} catch (e) {
  console.error('โŒ', e.message);
}

๐Ÿš€ Dependency Injection with Parameters

๐Ÿ’‰ Building a DI System

Creating a dependency injection system with parameter decorators:

// ๐ŸŽฏ DI Container
class DIContainer {
  private static services = new Map<string, any>();
  private static factories = new Map<string, () => any>();
  
  static register(token: string, service: any): void {
    this.services.set(token, service);
  }
  
  static registerFactory(token: string, factory: () => any): void {
    this.factories.set(token, factory);
  }
  
  static resolve<T>(token: string): T {
    if (this.services.has(token)) {
      return this.services.get(token);
    }
    
    if (this.factories.has(token)) {
      const factory = this.factories.get(token)!;
      const instance = factory();
      this.services.set(token, instance);
      return instance;
    }
    
    throw new Error(`Service ${token} not found in container`);
  }
  
  static clear(): void {
    this.services.clear();
    this.factories.clear();
  }
}

// ๐Ÿท๏ธ Injection metadata
const INJECT_KEY = Symbol('inject');

// ๐Ÿ’‰ Inject decorator
function Inject(token: string) {
  return function(target: any, propertyKey: string, parameterIndex: number) {
    const existingTokens = Reflect.getOwnMetadata(INJECT_KEY, target, propertyKey) || {};
    existingTokens[parameterIndex] = token;
    Reflect.defineMetadata(INJECT_KEY, existingTokens, target, propertyKey);
  };
}

// ๐Ÿ”„ Optional injection
function Optional(target: any, propertyKey: string, parameterIndex: number) {
  const key = `${propertyKey}_optional`;
  const existing = Reflect.getOwnMetadata(key, target) || [];
  existing.push(parameterIndex);
  Reflect.defineMetadata(key, existing, target);
}

// ๐Ÿ—๏ธ Resolve dependencies decorator
function ResolveDependencies(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  
  descriptor.value = function(...args: any[]) {
    const tokens = Reflect.getOwnMetadata(INJECT_KEY, target, propertyKey) || {};
    const optionalKey = `${propertyKey}_optional`;
    const optionalParams = Reflect.getOwnMetadata(optionalKey, target) || [];
    
    // Resolve injected dependencies
    Object.entries(tokens).forEach(([index, token]) => {
      const paramIndex = parseInt(index);
      
      // Skip if already provided
      if (args[paramIndex] !== undefined) return;
      
      try {
        args[paramIndex] = DIContainer.resolve(token as string);
      } catch (error) {
        if (!optionalParams.includes(paramIndex)) {
          throw error;
        }
        // Optional parameter, leave as undefined
      }
    });
    
    return originalMethod.apply(this, args);
  };
  
  return descriptor;
}

// ๐Ÿ  Services for injection
class Logger {
  log(message: string): void {
    console.log(`๐Ÿ“ [${new Date().toISOString()}] ${message}`);
  }
}

class Database {
  query(sql: string): any[] {
    console.log(`๐Ÿ—„๏ธ Executing: ${sql}`);
    return [];
  }
}

class Cache {
  get(key: string): any {
    console.log(`๐Ÿ’พ Cache get: ${key}`);
    return null;
  }
  
  set(key: string, value: any): void {
    console.log(`๐Ÿ’พ Cache set: ${key}`);
  }
}

class EmailService {
  send(to: string, subject: string, body: string): void {
    console.log(`๐Ÿ“ง Sending email to ${to}: ${subject}`);
  }
}

// Register services
DIContainer.register('logger', new Logger());
DIContainer.register('database', new Database());
DIContainer.register('cache', new Cache());
DIContainer.registerFactory('emailService', () => new EmailService());

// ๐ŸŽฏ Service using DI
class UserController {
  @ResolveDependencies
  getUser(
    userId: string,
    @Inject('database') db?: Database,
    @Inject('cache') cache?: Cache,
    @Inject('logger') logger?: Logger
  ): any {
    logger?.log(`Getting user ${userId}`);
    
    // Check cache first
    const cached = cache?.get(`user:${userId}`);
    if (cached) {
      logger?.log('User found in cache');
      return cached;
    }
    
    // Query database
    const user = db?.query(`SELECT * FROM users WHERE id = '${userId}'`)[0];
    cache?.set(`user:${userId}`, user);
    
    return user;
  }
  
  @ResolveDependencies
  createUser(
    userData: any,
    @Inject('database') db?: Database,
    @Inject('logger') logger?: Logger,
    @Inject('emailService') @Optional emailService?: EmailService
  ): void {
    logger?.log('Creating new user');
    db?.query(`INSERT INTO users VALUES (...)`);
    
    if (emailService) {
      emailService.send(userData.email, 'Welcome!', 'Thanks for signing up');
    }
  }
}

// ๐Ÿ’ซ Testing DI
const controller = new UserController();

// Dependencies are automatically injected
controller.getUser('123');
controller.createUser({ email: '[email protected]' });

// Can also provide dependencies manually
controller.getUser('456', new Database(), new Cache());

๐ŸŽจ Advanced Parameter Patterns

๐Ÿ”„ Parameter Transformation

Transforming parameters before method execution:

// ๐ŸŽฏ Transform metadata
const TRANSFORM_KEY = Symbol('transform');

// ๐Ÿ”„ Transform decorator
function Transform(transformer: (value: any) => any) {
  return function(target: any, propertyKey: string, parameterIndex: number) {
    const existing = Reflect.getOwnMetadata(TRANSFORM_KEY, target, propertyKey) || {};
    existing[parameterIndex] = transformer;
    Reflect.defineMetadata(TRANSFORM_KEY, existing, target, propertyKey);
  };
}

// ๐Ÿ“ Common transformers
const Transformers = {
  toLowerCase: (value: string) => value?.toLowerCase(),
  toUpperCase: (value: string) => value?.toUpperCase(),
  trim: (value: string) => value?.trim(),
  parseInt: (value: any) => parseInt(value, 10),
  parseFloat: (value: any) => parseFloat(value),
  toDate: (value: any) => new Date(value),
  toArray: (value: any) => Array.isArray(value) ? value : [value],
  sanitizeHtml: (value: string) => value?.replace(/<[^>]*>/g, '')
};

// ๐Ÿ—๏ธ Apply transforms decorator
function ApplyTransforms(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  
  descriptor.value = function(...args: any[]) {
    const transforms = Reflect.getOwnMetadata(TRANSFORM_KEY, target, propertyKey) || {};
    
    // Apply transformations
    Object.entries(transforms).forEach(([index, transformer]) => {
      const paramIndex = parseInt(index);
      if (args[paramIndex] !== undefined) {
        args[paramIndex] = (transformer as Function)(args[paramIndex]);
      }
    });
    
    return originalMethod.apply(this, args);
  };
  
  return descriptor;
}

// ๐ŸŽฏ Default value decorator
function Default(defaultValue: any) {
  return function(target: any, propertyKey: string, parameterIndex: number) {
    const key = `${propertyKey}_defaults`;
    const existing = Reflect.getOwnMetadata(key, target) || {};
    existing[parameterIndex] = defaultValue;
    Reflect.defineMetadata(key, existing, target);
  };
}

// ๐Ÿ—๏ธ Apply defaults decorator
function ApplyDefaults(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  
  descriptor.value = function(...args: any[]) {
    const key = `${propertyKey}_defaults`;
    const defaults = Reflect.getOwnMetadata(key, target) || {};
    
    // Apply default values
    Object.entries(defaults).forEach(([index, defaultValue]) => {
      const paramIndex = parseInt(index);
      if (args[paramIndex] === undefined) {
        args[paramIndex] = typeof defaultValue === 'function' ? defaultValue() : defaultValue;
      }
    });
    
    return originalMethod.apply(this, args);
  };
  
  return descriptor;
}

// ๐Ÿ  Example with transformations
class DataProcessor {
  @ApplyTransforms
  @ApplyDefaults
  processText(
    @Transform(Transformers.trim) 
    @Transform(Transformers.toLowerCase) 
    text: string,
    
    @Transform(Transformers.toArray)
    @Default([])
    tags: string[],
    
    @Transform(Transformers.sanitizeHtml)
    @Default('')
    description: string
  ): void {
    console.log('๐Ÿ“ Processing text:', { text, tags, description });
  }
  
  @ApplyTransforms
  @Validate
  createRecord(
    @Transform(Transformers.toUpperCase)
    @Pattern(/^[A-Z]{3}-\d{4}$/)
    id: string,
    
    @Transform(Transformers.toDate)
    @Default(() => new Date())
    createdAt: Date,
    
    @Transform(Transformers.parseInt)
    @Range(1, 100)
    @Default(1)
    priority: number
  ): void {
    console.log('๐Ÿ“Š Creating record:', { id, createdAt, priority });
  }
}

// ๐Ÿ’ซ Testing transformations
const processor = new DataProcessor();

// Test with transformations
processor.processText(
  '  Hello World  ',  // Will be trimmed and lowercased
  'tag1',             // Will be converted to array
  '<b>Bold</b> text'  // HTML will be sanitized
);

// Test with defaults
processor.processText('test'); // Uses default empty array and string

// Test combined transform and validation
processor.createRecord('abc-1234', '2024-01-01', '50');

๐ŸŽญ API Documentation Generation

Using parameter decorators for API documentation:

// ๐ŸŽฏ API documentation metadata
const API_PARAM_KEY = Symbol('apiParam');
const API_RESPONSE_KEY = Symbol('apiResponse');
const API_ENDPOINT_KEY = Symbol('apiEndpoint');

interface ApiParamMetadata {
  name: string;
  type: string;
  description: string;
  required: boolean;
  example?: any;
}

// ๐Ÿ“ API parameter decorator
function ApiParam(metadata: Omit<ApiParamMetadata, 'name'>) {
  return function(target: any, propertyKey: string, parameterIndex: number) {
    const existing = Reflect.getOwnMetadata(API_PARAM_KEY, target, propertyKey) || [];
    
    // Get parameter name from design metadata
    const paramTypes = Reflect.getMetadata('design:paramtypes', target, propertyKey);
    const paramNames = getParameterNames(target[propertyKey]);
    
    existing[parameterIndex] = {
      ...metadata,
      name: paramNames[parameterIndex] || `param${parameterIndex}`
    };
    
    Reflect.defineMetadata(API_PARAM_KEY, existing, target, propertyKey);
  };
}

// ๐ŸŒ API endpoint decorator
function ApiEndpoint(method: string, path: string, description: string) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    Reflect.defineMetadata(API_ENDPOINT_KEY, {
      method,
      path,
      description
    }, target, propertyKey);
    
    return descriptor;
  };
}

// ๐Ÿ“Š API response decorator
function ApiResponse(status: number, description: string, example?: any) {
  return function(target: any, propertyKey: string) {
    const existing = Reflect.getOwnMetadata(API_RESPONSE_KEY, target, propertyKey) || [];
    existing.push({ status, description, example });
    Reflect.defineMetadata(API_RESPONSE_KEY, existing, target, propertyKey);
  };
}

// ๐Ÿ”ง Helper to extract parameter names
function getParameterNames(func: Function): string[] {
  const fnStr = func.toString();
  const match = fnStr.match(/\(([^)]*)\)/);
  if (!match) return [];
  
  return match[1].split(',').map(param => {
    const trimmed = param.trim();
    const name = trimmed.split(/[:=]/)[0].trim();
    return name;
  });
}

// ๐Ÿ“š Generate API documentation
function generateApiDocs(controller: any): string {
  const className = controller.constructor.name;
  let docs = `# ${className} API Documentation\n\n`;
  
  const prototype = controller.constructor.prototype;
  const methods = Object.getOwnPropertyNames(prototype)
    .filter(name => name !== 'constructor');
  
  methods.forEach(methodName => {
    const endpoint = Reflect.getMetadata(API_ENDPOINT_KEY, prototype, methodName);
    if (!endpoint) return;
    
    docs += `## ${endpoint.method} ${endpoint.path}\n`;
    docs += `${endpoint.description}\n\n`;
    
    // Parameters
    const params = Reflect.getMetadata(API_PARAM_KEY, prototype, methodName) || [];
    if (params.length > 0) {
      docs += `### Parameters\n\n`;
      params.forEach((param: ApiParamMetadata, index: number) => {
        if (param) {
          docs += `- **${param.name}** (${param.type})${param.required ? ' *required*' : ''}: ${param.description}\n`;
          if (param.example !== undefined) {
            docs += `  - Example: \`${JSON.stringify(param.example)}\`\n`;
          }
        }
      });
      docs += '\n';
    }
    
    // Responses
    const responses = Reflect.getMetadata(API_RESPONSE_KEY, prototype, methodName) || [];
    if (responses.length > 0) {
      docs += `### Responses\n\n`;
      responses.forEach((response: any) => {
        docs += `- **${response.status}**: ${response.description}\n`;
        if (response.example) {
          docs += `  \`\`\`json\n  ${JSON.stringify(response.example, null, 2)}\n  \`\`\`\n`;
        }
      });
      docs += '\n';
    }
  });
  
  return docs;
}

// ๐Ÿ  API Controller example
class UserAPI {
  @ApiEndpoint('GET', '/users/:id', 'Get user by ID')
  @ApiResponse(200, 'User found', { id: '123', name: 'John Doe', email: '[email protected]' })
  @ApiResponse(404, 'User not found')
  getUser(
    @ApiParam({
      type: 'string',
      description: 'User ID (UUID format)',
      required: true,
      example: '123e4567-e89b-12d3-a456-426614174000'
    })
    id: string
  ): any {
    return { id, name: 'John Doe' };
  }
  
  @ApiEndpoint('POST', '/users', 'Create a new user')
  @ApiResponse(201, 'User created successfully', { id: '123', name: 'John Doe' })
  @ApiResponse(400, 'Invalid user data')
  createUser(
    @ApiParam({
      type: 'string',
      description: 'Username (3-20 characters)',
      required: true,
      example: 'johndoe'
    })
    username: string,
    
    @ApiParam({
      type: 'string',
      description: 'Email address',
      required: true,
      example: '[email protected]'
    })
    email: string,
    
    @ApiParam({
      type: 'string',
      description: 'User password (min 8 characters)',
      required: true
    })
    password: string,
    
    @ApiParam({
      type: 'object',
      description: 'Additional user profile data',
      required: false,
      example: { bio: 'Software developer', location: 'New York' }
    })
    profile?: any
  ): any {
    return { id: '123', name: username };
  }
}

// ๐Ÿ’ซ Generate documentation
const userApi = new UserAPI();
console.log(generateApiDocs(userApi));

๐ŸŽฎ Hands-On Exercise

Letโ€™s build a complete RPC system using parameter decorators!

๐Ÿ“ Challenge: Type-Safe RPC System

Create an RPC (Remote Procedure Call) system that:

  1. Uses parameter decorators for type checking
  2. Supports async method calls
  3. Provides automatic serialization/deserialization
  4. Includes error handling and validation
// Your challenge: Implement this RPC system
interface RpcOptions {
  timeout?: number;
  retries?: number;
}

// Decorators to implement:
// @RpcMethod(name) - Mark method as RPC endpoint
// @RpcParam(type) - Define parameter type for serialization
// @RpcAuth - Require authentication
// @RpcCache(ttl) - Cache results

// Example usage to support:
class MathService {
  @RpcMethod('add')
  @RpcCache(60000) // Cache for 1 minute
  add(
    @RpcParam('number') a: number,
    @RpcParam('number') b: number
  ): number {
    return a + b;
  }
  
  @RpcMethod('calculate')
  @RpcAuth
  calculate(
    @RpcParam('string') expression: string,
    @RpcParam('object') variables?: Record<string, number>
  ): number {
    // Evaluate expression with variables
    return 0;
  }
}

// Client usage:
const client = new RpcClient('http://localhost:3000');
const result = await client.call('add', [5, 3]); // 8

๐Ÿ’ก Solution

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

// ๐ŸŽฏ RPC Metadata keys
const RPC_METHOD_KEY = Symbol('rpcMethod');
const RPC_PARAM_KEY = Symbol('rpcParam');
const RPC_AUTH_KEY = Symbol('rpcAuth');
const RPC_CACHE_KEY = Symbol('rpcCache');

// ๐Ÿ“Š Type definitions
type RpcParamType = 'string' | 'number' | 'boolean' | 'object' | 'array' | 'any';

interface RpcMethodMetadata {
  name: string;
  requiresAuth: boolean;
  cacheTtl?: number;
  paramTypes: RpcParamType[];
}

interface RpcRequest {
  method: string;
  params: any[];
  id: string;
  auth?: string;
}

interface RpcResponse {
  result?: any;
  error?: { code: number; message: string };
  id: string;
}

// ๐Ÿ—๏ธ RPC Method decorator
function RpcMethod(name: string) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    Reflect.defineMetadata(RPC_METHOD_KEY, name, target, propertyKey);
    return descriptor;
  };
}

// ๐Ÿ“ RPC Parameter decorator
function RpcParam(type: RpcParamType) {
  return function(target: any, propertyKey: string, parameterIndex: number) {
    const existing = Reflect.getOwnMetadata(RPC_PARAM_KEY, target, propertyKey) || [];
    existing[parameterIndex] = type;
    Reflect.defineMetadata(RPC_PARAM_KEY, existing, target, propertyKey);
  };
}

// ๐Ÿ” RPC Auth decorator
function RpcAuth(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  Reflect.defineMetadata(RPC_AUTH_KEY, true, target, propertyKey);
  return descriptor;
}

// ๐Ÿ’พ RPC Cache decorator
function RpcCache(ttl: number) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    Reflect.defineMetadata(RPC_CACHE_KEY, ttl, target, propertyKey);
    
    const cache = new Map<string, { value: any; expires: number }>();
    const originalMethod = descriptor.value;
    
    descriptor.value = function(...args: any[]) {
      const cacheKey = JSON.stringify(args);
      const cached = cache.get(cacheKey);
      
      if (cached && cached.expires > Date.now()) {
        console.log(`๐Ÿ’พ Cache hit for ${propertyKey}`);
        return cached.value;
      }
      
      const result = originalMethod.apply(this, args);
      cache.set(cacheKey, {
        value: result,
        expires: Date.now() + ttl
      });
      
      return result;
    };
    
    return descriptor;
  };
}

// ๐ŸŽฏ RPC Server
class RpcServer {
  private services = new Map<string, any>();
  private methods = new Map<string, { service: any; method: string; metadata: RpcMethodMetadata }>();
  
  registerService(service: any): void {
    this.services.set(service.constructor.name, service);
    
    // Scan for RPC methods
    const prototype = service.constructor.prototype;
    const propertyNames = Object.getOwnPropertyNames(prototype);
    
    propertyNames.forEach(propertyName => {
      if (propertyName === 'constructor') return;
      
      const rpcName = Reflect.getMetadata(RPC_METHOD_KEY, prototype, propertyName);
      if (!rpcName) return;
      
      const paramTypes = Reflect.getMetadata(RPC_PARAM_KEY, prototype, propertyName) || [];
      const requiresAuth = Reflect.getMetadata(RPC_AUTH_KEY, prototype, propertyName) || false;
      const cacheTtl = Reflect.getMetadata(RPC_CACHE_KEY, prototype, propertyName);
      
      this.methods.set(rpcName, {
        service,
        method: propertyName,
        metadata: {
          name: rpcName,
          requiresAuth,
          cacheTtl,
          paramTypes
        }
      });
      
      console.log(`๐Ÿ“ก Registered RPC method: ${rpcName}`);
    });
  }
  
  async handleRequest(request: RpcRequest): Promise<RpcResponse> {
    const methodInfo = this.methods.get(request.method);
    
    if (!methodInfo) {
      return {
        error: { code: -32601, message: 'Method not found' },
        id: request.id
      };
    }
    
    // Check authentication
    if (methodInfo.metadata.requiresAuth && !request.auth) {
      return {
        error: { code: -32003, message: 'Authentication required' },
        id: request.id
      };
    }
    
    // Validate parameters
    const { paramTypes } = methodInfo.metadata;
    for (let i = 0; i < paramTypes.length; i++) {
      if (!this.validateParam(request.params[i], paramTypes[i])) {
        return {
          error: { 
            code: -32602, 
            message: `Invalid parameter at index ${i}: expected ${paramTypes[i]}` 
          },
          id: request.id
        };
      }
    }
    
    try {
      // Execute method
      const result = await methodInfo.service[methodInfo.method](...request.params);
      
      return {
        result,
        id: request.id
      };
    } catch (error) {
      return {
        error: { 
          code: -32603, 
          message: error instanceof Error ? error.message : 'Internal error' 
        },
        id: request.id
      };
    }
  }
  
  private validateParam(value: any, type: RpcParamType): boolean {
    switch (type) {
      case 'string':
        return typeof value === 'string';
      case 'number':
        return typeof value === 'number' && !isNaN(value);
      case 'boolean':
        return typeof value === 'boolean';
      case 'array':
        return Array.isArray(value);
      case 'object':
        return value !== null && typeof value === 'object' && !Array.isArray(value);
      case 'any':
        return true;
      default:
        return false;
    }
  }
}

// ๐ŸŒ RPC Client
class RpcClient {
  private nextId = 1;
  
  constructor(private endpoint: string, private authToken?: string) {}
  
  async call(method: string, params: any[] = []): Promise<any> {
    const request: RpcRequest = {
      method,
      params,
      id: String(this.nextId++),
      auth: this.authToken
    };
    
    console.log(`๐Ÿ“ค RPC Request:`, request);
    
    // In real implementation, this would make HTTP request
    // For demo, we'll simulate with direct server call
    const response = await globalServer.handleRequest(request);
    
    console.log(`๐Ÿ“ฅ RPC Response:`, response);
    
    if (response.error) {
      throw new Error(`RPC Error ${response.error.code}: ${response.error.message}`);
    }
    
    return response.result;
  }
  
  setAuth(token: string): void {
    this.authToken = token;
  }
}

// ๐Ÿ  Example services
class MathService {
  @RpcMethod('add')
  @RpcCache(60000)
  add(
    @RpcParam('number') a: number,
    @RpcParam('number') b: number
  ): number {
    console.log(`โž• Calculating ${a} + ${b}`);
    return a + b;
  }
  
  @RpcMethod('multiply')
  multiply(
    @RpcParam('number') a: number,
    @RpcParam('number') b: number
  ): number {
    return a * b;
  }
  
  @RpcMethod('calculate')
  @RpcAuth
  calculate(
    @RpcParam('string') expression: string,
    @RpcParam('object') variables?: Record<string, number>
  ): number {
    console.log(`๐Ÿงฎ Evaluating: ${expression} with`, variables);
    
    // Simple expression evaluator
    let expr = expression;
    if (variables) {
      Object.entries(variables).forEach(([key, value]) => {
        expr = expr.replace(new RegExp(`\\b${key}\\b`, 'g'), String(value));
      });
    }
    
    // WARNING: In production, use a proper expression parser
    return Function(`"use strict"; return (${expr})`)();
  }
}

class UserService {
  private users = new Map<string, any>();
  
  @RpcMethod('createUser')
  @RpcAuth
  createUser(
    @RpcParam('string') username: string,
    @RpcParam('string') email: string,
    @RpcParam('object') profile?: any
  ): { id: string; username: string } {
    const id = Date.now().toString();
    const user = { id, username, email, profile };
    this.users.set(id, user);
    
    console.log(`๐Ÿ‘ค Created user:`, user);
    return { id, username };
  }
  
  @RpcMethod('getUser')
  @RpcCache(30000)
  getUser(@RpcParam('string') id: string): any {
    const user = this.users.get(id);
    if (!user) {
      throw new Error('User not found');
    }
    return user;
  }
}

// ๐Ÿ’ซ Testing the RPC system
const globalServer = new RpcServer();
globalServer.registerService(new MathService());
globalServer.registerService(new UserService());

async function testRpcSystem() {
  console.log('=== RPC System Demo ===\n');
  
  // Test without auth
  const client = new RpcClient('http://localhost:3000');
  
  // Test math operations
  const sum = await client.call('add', [5, 3]);
  console.log('Sum:', sum); // 8
  
  // Test caching
  await client.call('add', [5, 3]); // Should hit cache
  
  const product = await client.call('multiply', [4, 7]);
  console.log('Product:', product); // 28
  
  // Test auth required method (will fail)
  try {
    await client.call('calculate', ['x + y', { x: 10, y: 20 }]);
  } catch (e) {
    console.error('Expected error:', e.message);
  }
  
  // Test with auth
  client.setAuth('secret-token');
  const result = await client.call('calculate', ['x * 2 + y', { x: 10, y: 5 }]);
  console.log('Calculation result:', result); // 25
  
  // Test user service
  const user = await client.call('createUser', ['johndoe', '[email protected]', { age: 30 }]);
  console.log('Created user:', user);
  
  // Test parameter validation
  try {
    await client.call('add', ['not a number', 5]);
  } catch (e) {
    console.error('Expected validation error:', e.message);
  }
}

testRpcSystem();

๐ŸŽฏ Summary

Youโ€™ve mastered parameter decorators in TypeScript! ๐ŸŽ‰ You learned how to:

  • ๐Ÿท๏ธ Create parameter decorators for metadata collection
  • โœ… Implement comprehensive parameter validation
  • ๐Ÿ’‰ Build dependency injection systems
  • ๐Ÿ”„ Transform parameters before execution
  • ๐Ÿ“š Generate API documentation from decorators
  • ๐ŸŒ Create type-safe RPC systems

Parameter decorators provide the foundation for powerful frameworks and libraries, enabling sophisticated patterns like dependency injection, validation, and API documentation generation!

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