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:
- Uses parameter decorators for type checking
- Supports async method calls
- Provides automatic serialization/deserialization
- 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! ๐