+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 294 of 354

๐Ÿ“˜ Proxy Pattern: Controlled Access

Master proxy pattern: controlled access in TypeScript with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿš€Intermediate
25 min read

Prerequisites

  • Basic understanding of JavaScript ๐Ÿ“
  • TypeScript installation โšก
  • VS Code or preferred IDE ๐Ÿ’ป

What you'll learn

  • Understand the concept fundamentals ๐ŸŽฏ
  • Apply the concept in real projects ๐Ÿ—๏ธ
  • Debug common issues ๐Ÿ›
  • Write type-safe code โœจ

๐ŸŽฏ Introduction

Have you ever had a friend who acted as your โ€œrepresentativeโ€ when you couldnโ€™t be somewhere? ๐Ÿค Thatโ€™s exactly what the Proxy Pattern does in programming! It creates a stand-in object that controls access to another object, adding an extra layer of control and functionality.

In this tutorial, youโ€™ll discover how to implement the Proxy Pattern in TypeScript to create powerful, controlled access to your objects. Whether youโ€™re building secure systems, optimizing performance, or adding logging capabilities, the Proxy Pattern has got your back! ๐Ÿ’ช

What Youโ€™ll Learn Today ๐ŸŽ“

  • The fundamentals of the Proxy Pattern and when to use it ๐Ÿง 
  • How to implement different types of proxies in TypeScript ๐Ÿ›ก๏ธ
  • Real-world applications like caching, validation, and security ๐Ÿ”
  • Best practices and common pitfalls to avoid ๐Ÿš€

Letโ€™s dive in and become proxy pattern masters! ๐ŸŽ‰

๐Ÿ“š Understanding Proxy Pattern

Think of a proxy like a security guard at a building entrance ๐Ÿ‘ฎโ€โ™‚๏ธ. Before you can enter the building (access the real object), you must go through the security guard (the proxy). The guard can check your credentials, log your entry, or even deny access entirely!

What is the Proxy Pattern? ๐Ÿค”

The Proxy Pattern provides a placeholder or surrogate for another object to control access to it. Itโ€™s like having a personal assistant who screens your calls and manages your schedule! ๐Ÿ“ฑ

Key Components ๐Ÿ”ง

  1. Subject Interface: Defines the common interface for RealSubject and Proxy
  2. RealSubject: The actual object that the proxy represents
  3. Proxy: Maintains a reference to the RealSubject and controls access to it
  4. Client: Interacts with RealSubject through the Proxy

Types of Proxies ๐ŸŽญ

  • Virtual Proxy: Delays expensive object creation (lazy loading) ๐Ÿ’ค
  • Protection Proxy: Controls access based on permissions ๐Ÿ”’
  • Remote Proxy: Represents objects in different address spaces ๐ŸŒ
  • Caching Proxy: Stores results to avoid repeated calculations ๐Ÿ’พ
  • Logging Proxy: Adds logging functionality transparently ๐Ÿ“

๐Ÿ”ง Basic Syntax and Usage

Letโ€™s start with a simple example to understand the basic structure of the Proxy Pattern in TypeScript! ๐Ÿš€

// ๐ŸŽฏ Define the subject interface
interface Image {
  display(): void;
}

// ๐Ÿ“ธ Real subject - the actual image
class RealImage implements Image {
  private filename: string;

  constructor(filename: string) {
    this.filename = filename;
    this.loadFromDisk(); // ๐Ÿ’พ Expensive operation!
  }

  private loadFromDisk(): void {
    console.log(`๐Ÿ“‚ Loading ${this.filename} from disk...`);
  }

  display(): void {
    console.log(`๐Ÿ–ผ๏ธ Displaying ${this.filename}`);
  }
}

// ๐Ÿ›ก๏ธ Proxy - controls access to the real image
class ProxyImage implements Image {
  private realImage: RealImage | null = null;
  private filename: string;

  constructor(filename: string) {
    this.filename = filename;
  }

  display(): void {
    // ๐Ÿ’ก Lazy loading - create only when needed!
    if (this.realImage === null) {
      console.log('๐Ÿ”„ First time accessing, creating real image...');
      this.realImage = new RealImage(this.filename);
    }
    this.realImage.display();
  }
}

// ๐ŸŽฎ Using the proxy
const image1 = new ProxyImage('vacation-photo.jpg');
const image2 = new ProxyImage('family-portrait.jpg');

console.log('๐ŸŽฌ Images created but not loaded yet!');

// Images are loaded only when displayed
image1.display(); // Loads and displays
image1.display(); // Just displays (already loaded!)

Using JavaScriptโ€™s Built-in Proxy ๐ŸŒŸ

TypeScript also supports JavaScriptโ€™s native Proxy object for even more flexibility!

// ๐ŸŽฏ Target object
const user = {
  name: 'Alice',
  age: 25,
  role: 'admin'
};

// ๐Ÿ›ก๏ธ Create a proxy with custom behavior
const secureUser = new Proxy(user, {
  get(target, property) {
    console.log(`๐Ÿ“– Accessing property: ${String(property)}`);
    return target[property as keyof typeof target];
  },
  
  set(target, property, value) {
    console.log(`โœ๏ธ Setting ${String(property)} = ${value}`);
    
    // ๐Ÿ”’ Add validation
    if (property === 'age' && typeof value === 'number' && value < 0) {
      throw new Error('โŒ Age cannot be negative!');
    }
    
    target[property as keyof typeof target] = value;
    return true;
  }
});

// ๐ŸŽฎ Test it out!
console.log(secureUser.name); // Logs access, returns 'Alice'
secureUser.age = 30; // Logs the change

๐Ÿ’ก Practical Examples

Letโ€™s explore some real-world scenarios where the Proxy Pattern shines! ๐ŸŒŸ

Example 1: API Cache Proxy ๐Ÿš€

Imagine youโ€™re building a weather app that makes expensive API calls. Letโ€™s create a caching proxy to improve performance!

// ๐ŸŒค๏ธ Weather service interface
interface WeatherService {
  getWeather(city: string): Promise<string>;
}

// ๐ŸŒ Real weather service (makes actual API calls)
class RealWeatherService implements WeatherService {
  async getWeather(city: string): Promise<string> {
    console.log(`๐ŸŒ Making API call for ${city}...`);
    // Simulate API call
    await new Promise(resolve => setTimeout(resolve, 2000));
    return `${city}: Sunny, 25ยฐC โ˜€๏ธ`;
  }
}

// ๐Ÿ’พ Caching proxy
class CachedWeatherService implements WeatherService {
  private realService: RealWeatherService;
  private cache: Map<string, { data: string; timestamp: number }> = new Map();
  private cacheTimeout = 60000; // 1 minute cache

  constructor() {
    this.realService = new RealWeatherService();
  }

  async getWeather(city: string): Promise<string> {
    const cached = this.cache.get(city);
    
    // ๐ŸŽฏ Check if we have valid cached data
    if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
      console.log(`๐Ÿ’พ Returning cached weather for ${city}`);
      return cached.data;
    }

    // ๐Ÿ”„ Fetch fresh data
    console.log(`๐Ÿ”„ Cache miss for ${city}, fetching fresh data...`);
    const weather = await this.realService.getWeather(city);
    
    // ๐Ÿ’พ Store in cache
    this.cache.set(city, { data: weather, timestamp: Date.now() });
    
    return weather;
  }
}

// ๐ŸŽฎ Usage
const weatherService = new CachedWeatherService();

async function checkWeather() {
  // First call - hits the API
  console.log(await weatherService.getWeather('London'));
  
  // Second call - returns from cache! ๐Ÿš€
  console.log(await weatherService.getWeather('London'));
  
  // Different city - hits the API
  console.log(await weatherService.getWeather('Tokyo'));
}

checkWeather();

Example 2: Permission-Based Access Proxy ๐Ÿ”

Letโ€™s create a document management system with access control!

// ๐Ÿ“„ Document interface
interface Document {
  read(): string;
  write(content: string): void;
  delete(): void;
}

// ๐Ÿ‘ค User roles
type UserRole = 'admin' | 'editor' | 'viewer';

// ๐Ÿ“‹ Real document
class RealDocument implements Document {
  private content: string;
  private title: string;

  constructor(title: string, content: string) {
    this.title = title;
    this.content = content;
  }

  read(): string {
    return this.content;
  }

  write(content: string): void {
    this.content = content;
    console.log(`โœ๏ธ Document updated: ${this.title}`);
  }

  delete(): void {
    console.log(`๐Ÿ—‘๏ธ Document deleted: ${this.title}`);
  }
}

// ๐Ÿ›ก๏ธ Security proxy
class SecureDocument implements Document {
  private document: RealDocument;
  private userRole: UserRole;

  constructor(document: RealDocument, userRole: UserRole) {
    this.document = document;
    this.userRole = userRole;
  }

  read(): string {
    console.log(`๐Ÿ‘๏ธ ${this.userRole} is reading the document`);
    return this.document.read();
  }

  write(content: string): void {
    if (this.userRole === 'viewer') {
      throw new Error('โŒ Viewers cannot edit documents!');
    }
    console.log(`โœ… ${this.userRole} has write permission`);
    this.document.write(content);
  }

  delete(): void {
    if (this.userRole !== 'admin') {
      throw new Error('โŒ Only admins can delete documents!');
    }
    console.log(`โœ… Admin deleting document`);
    this.document.delete();
  }
}

// ๐ŸŽฎ Test different user permissions
const doc = new RealDocument('Secret Plans', 'Top secret content ๐Ÿคซ');

// Different users with different permissions
const adminDoc = new SecureDocument(doc, 'admin');
const editorDoc = new SecureDocument(doc, 'editor');
const viewerDoc = new SecureDocument(doc, 'viewer');

// Everyone can read โœ…
console.log(viewerDoc.read());

// Editor can write โœ…
editorDoc.write('Updated content ๐Ÿ“');

// Viewer cannot write โŒ
try {
  viewerDoc.write('Hacker attempt! ๐Ÿ‘พ');
} catch (error) {
  console.log(error.message);
}

Example 3: Virtual Scroll Proxy ๐Ÿ“œ

Letโ€™s create a virtual scrolling system for handling large lists efficiently!

// ๐Ÿ“Š Data item interface
interface ListItem {
  id: number;
  render(): HTMLElement;
}

// ๐ŸŽฏ Real list item (expensive to create)
class RealListItem implements ListItem {
  constructor(public id: number, private data: string) {
    console.log(`๐Ÿ—๏ธ Creating expensive item ${id}`);
  }

  render(): HTMLElement {
    const element = document.createElement('div');
    element.textContent = `Item ${this.id}: ${this.data}`;
    element.className = 'list-item';
    return element;
  }
}

// ๐Ÿš€ Lightweight proxy
class VirtualListItem implements ListItem {
  private realItem: RealListItem | null = null;

  constructor(public id: number, private dataLoader: () => string) {}

  render(): HTMLElement {
    // ๐Ÿ’ก Create real item only when rendering
    if (!this.realItem) {
      console.log(`โšก Lazy loading item ${this.id}`);
      this.realItem = new RealListItem(this.id, this.dataLoader());
    }
    return this.realItem.render();
  }
}

// ๐Ÿ“œ Virtual scroll manager
class VirtualScrollList {
  private items: VirtualListItem[] = [];
  private visibleRange = { start: 0, end: 10 };

  constructor(totalItems: number) {
    // ๐ŸŽฏ Create lightweight proxies for all items
    for (let i = 0; i < totalItems; i++) {
      this.items.push(
        new VirtualListItem(i, () => `Data for item ${i} ๐Ÿ“ฆ`)
      );
    }
    console.log(`โœจ Created ${totalItems} virtual items!`);
  }

  renderVisibleItems(): HTMLElement[] {
    console.log(`๐Ÿ–ผ๏ธ Rendering items ${this.visibleRange.start}-${this.visibleRange.end}`);
    
    const rendered: HTMLElement[] = [];
    for (let i = this.visibleRange.start; i < this.visibleRange.end; i++) {
      if (this.items[i]) {
        rendered.push(this.items[i].render());
      }
    }
    return rendered;
  }

  scrollTo(start: number): void {
    this.visibleRange = { start, end: start + 10 };
  }
}

// ๐ŸŽฎ Usage with thousands of items!
const hugeList = new VirtualScrollList(10000);

// Only renders what's visible ๐Ÿš€
hugeList.renderVisibleItems();

// Scroll down
hugeList.scrollTo(100);
hugeList.renderVisibleItems();

๐Ÿš€ Advanced Concepts

Ready to level up your Proxy Pattern skills? Letโ€™s explore some advanced techniques! ๐ŸŽฏ

Dynamic Property Validation ๐Ÿ”

Create proxies that validate object properties dynamically:

// ๐ŸŽฏ Type-safe validation proxy
type ValidationRule<T> = {
  validate: (value: T) => boolean;
  message: string;
};

type ValidationSchema<T> = {
  [K in keyof T]?: ValidationRule<T[K]>;
};

function createValidatedProxy<T extends object>(
  target: T,
  schema: ValidationSchema<T>
): T {
  return new Proxy(target, {
    set(obj, prop, value) {
      const key = prop as keyof T;
      const rule = schema[key];

      if (rule && !rule.validate(value)) {
        throw new Error(`โŒ Validation failed for ${String(key)}: ${rule.message}`);
      }

      obj[key] = value;
      console.log(`โœ… ${String(key)} validated and set to ${value}`);
      return true;
    }
  });
}

// ๐Ÿช Example: Product with validation
interface Product {
  name: string;
  price: number;
  stock: number;
}

const productSchema: ValidationSchema<Product> = {
  name: {
    validate: (v) => v.length > 0 && v.length <= 100,
    message: 'Name must be 1-100 characters'
  },
  price: {
    validate: (v) => v > 0 && v <= 10000,
    message: 'Price must be between 0 and 10000'
  },
  stock: {
    validate: (v) => v >= 0 && Number.isInteger(v),
    message: 'Stock must be a non-negative integer'
  }
};

// ๐ŸŽฎ Create validated product
const product = createValidatedProxy<Product>(
  { name: '', price: 0, stock: 0 },
  productSchema
);

// Test validations
product.name = 'Gaming Laptop ๐ŸŽฎ'; // โœ… Valid
product.price = 1299.99; // โœ… Valid

try {
  product.price = -50; // โŒ Will throw error
} catch (error) {
  console.log(error.message);
}

Revocable Proxies ๐Ÿ”

Create proxies that can be revoked for enhanced security:

// ๐Ÿ”’ Secure data access with revocable proxy
class SecureDataAccess<T extends object> {
  private revocableProxy: { proxy: T; revoke: () => void };
  private accessLog: string[] = [];
  private isRevoked = false;

  constructor(private sensitiveData: T, private accessDuration: number) {
    this.revocableProxy = Proxy.revocable(sensitiveData, {
      get: (target, prop) => {
        if (this.isRevoked) {
          throw new Error('โŒ Access has been revoked!');
        }
        
        const access = `Read ${String(prop)} at ${new Date().toISOString()}`;
        this.accessLog.push(access);
        console.log(`๐Ÿ“– ${access}`);
        
        return target[prop as keyof T];
      }
    });

    // ๐Ÿ• Auto-revoke after duration
    setTimeout(() => {
      this.revoke();
    }, this.accessDuration);
  }

  getProxy(): T {
    return this.revocableProxy.proxy;
  }

  revoke(): void {
    console.log('๐Ÿ”’ Revoking access to sensitive data...');
    this.revocableProxy.revoke();
    this.isRevoked = true;
  }

  getAccessLog(): string[] {
    return [...this.accessLog];
  }
}

// ๐ŸŽฎ Usage
const sensitiveInfo = {
  creditCard: '1234-5678-9012-3456 ๐Ÿ’ณ',
  ssn: '123-45-6789 ๐Ÿ”',
  password: 'super-secret-123 ๐Ÿคซ'
};

// Grant 5-second access
const secureAccess = new SecureDataAccess(sensitiveInfo, 5000);
const proxy = secureAccess.getProxy();

// Access data while allowed
console.log(proxy.creditCard); // โœ… Works

// After 5 seconds, access is revoked automatically!
setTimeout(() => {
  try {
    console.log(proxy.ssn); // โŒ Will throw error
  } catch (error) {
    console.log(error.message);
  }
  
  // View access log ๐Ÿ“Š
  console.log('Access Log:', secureAccess.getAccessLog());
}, 6000);

Proxy Chains ๐Ÿ”—

Combine multiple proxies for layered functionality:

// ๐ŸŽฏ Chainable proxy behaviors
type ProxyHandler<T> = {
  name: string;
  handler: ProxyHandler<T>;
};

// ๐Ÿ“ Logging behavior
function loggingProxy<T extends object>(target: T): T {
  return new Proxy(target, {
    get(obj, prop) {
      console.log(`๐Ÿ“– [LOG] Getting ${String(prop)}`);
      return obj[prop as keyof T];
    },
    set(obj, prop, value) {
      console.log(`โœ๏ธ [LOG] Setting ${String(prop)} = ${value}`);
      obj[prop as keyof T] = value;
      return true;
    }
  });
}

// โฑ๏ธ Performance monitoring behavior
function performanceProxy<T extends object>(target: T): T {
  return new Proxy(target, {
    get(obj, prop) {
      const start = performance.now();
      const result = obj[prop as keyof T];
      const duration = performance.now() - start;
      console.log(`โฑ๏ธ [PERF] ${String(prop)} took ${duration.toFixed(2)}ms`);
      return result;
    }
  });
}

// ๐Ÿ’พ Caching behavior
function cachingProxy<T extends object>(target: T): T {
  const cache = new Map<string | symbol, unknown>();
  
  return new Proxy(target, {
    get(obj, prop) {
      const key = prop as keyof T;
      if (cache.has(prop)) {
        console.log(`๐Ÿ’พ [CACHE] Hit for ${String(prop)}`);
        return cache.get(prop);
      }
      
      console.log(`๐Ÿ”„ [CACHE] Miss for ${String(prop)}`);
      const value = obj[key];
      cache.set(prop, value);
      return value;
    }
  });
}

// ๐Ÿ”— Create proxy chain
function createProxyChain<T extends object>(
  target: T,
  ...proxies: ((obj: T) => T)[]
): T {
  return proxies.reduce((acc, proxyFn) => proxyFn(acc), target);
}

// ๐ŸŽฎ Example usage
const dataService = {
  fetchUser: () => {
    console.log('๐ŸŒ Fetching user from database...');
    return { id: 1, name: 'Alice ๐Ÿ‘ฉ' };
  },
  fetchPosts: () => {
    console.log('๐ŸŒ Fetching posts from database...');
    return ['Post 1 ๐Ÿ“', 'Post 2 ๐Ÿ“', 'Post 3 ๐Ÿ“'];
  }
};

// Apply multiple proxy behaviors! ๐Ÿš€
const enhancedService = createProxyChain(
  dataService,
  cachingProxy,
  performanceProxy,
  loggingProxy
);

// First call - all behaviors active
enhancedService.fetchUser();

// Second call - cache hit! ๐Ÿ’พ
enhancedService.fetchUser();

โš ๏ธ Common Pitfalls and Solutions

Letโ€™s explore common mistakes and how to avoid them! ๐Ÿ›ก๏ธ

Pitfall 1: Performance Overhead ๐ŸŒ

โŒ Wrong Way:

// Creating proxies for every single object
class DataStore {
  private items: any[] = [];

  addItem(item: any): void {
    // โŒ Creating a proxy for each item is expensive!
    const proxiedItem = new Proxy(item, {
      get(target, prop) {
        console.log(`Accessing ${String(prop)}`);
        return target[prop];
      }
    });
    this.items.push(proxiedItem);
  }
}

โœ… Right Way:

// Use proxies strategically
class OptimizedDataStore {
  private items: any[] = [];
  private accessCounts = new Map<number, number>();

  addItem(item: any): void {
    const index = this.items.length;
    this.items.push(item);
    
    // โœ… Track access patterns without proxy overhead
    this.accessCounts.set(index, 0);
  }

  getItem(index: number): any {
    this.accessCounts.set(index, (this.accessCounts.get(index) || 0) + 1);
    
    // โœ… Only create proxy for frequently accessed items
    if (this.accessCounts.get(index)! > 10) {
      console.log(`๐Ÿ”ฅ Creating optimized proxy for hot item ${index}`);
      return new Proxy(this.items[index], {
        get(target, prop) {
          console.log(`โšก Fast access to ${String(prop)}`);
          return target[prop];
        }
      });
    }
    
    return this.items[index];
  }
}

Pitfall 2: Breaking Object Identity ๐ŸŽญ

โŒ Wrong Way:

// Proxy breaks object identity checks
const user = { id: 1, name: 'Bob' };
const proxyUser = new Proxy(user, {});

// โŒ This will be false!
console.log(user === proxyUser); // false

// โŒ Can cause issues with Maps/Sets
const userSet = new Set([user]);
console.log(userSet.has(proxyUser)); // false!

โœ… Right Way:

// Maintain reference to original object
class IdentityPreservingProxy<T extends object> {
  private static proxyMap = new WeakMap<object, object>();

  static create<T extends object>(target: T): T {
    // โœ… Return existing proxy if already created
    const existing = this.proxyMap.get(target);
    if (existing) return existing as T;

    const proxy = new Proxy(target, {
      get(obj, prop) {
        console.log(`๐Ÿ“– Accessing ${String(prop)}`);
        return obj[prop as keyof T];
      }
    });

    // โœ… Store proxy reference
    this.proxyMap.set(target, proxy);
    return proxy;
  }

  static getOriginal<T extends object>(proxy: T): T | undefined {
    // โœ… Utility to get original object
    for (const [original, stored] of this.proxyMap) {
      if (stored === proxy) return original as T;
    }
    return undefined;
  }
}

// โœ… Usage
const user2 = { id: 2, name: 'Charlie' };
const proxy1 = IdentityPreservingProxy.create(user2);
const proxy2 = IdentityPreservingProxy.create(user2);

console.log(proxy1 === proxy2); // true! Same proxy returned ๐ŸŽ‰

Pitfall 3: Hidden Errors ๐Ÿ›

โŒ Wrong Way:

// Silently catching errors
const dangerousProxy = new Proxy({}, {
  get(target, prop) {
    try {
      return target[prop as keyof typeof target];
    } catch (error) {
      // โŒ Hiding errors makes debugging nightmare!
      return undefined;
    }
  }
});

โœ… Right Way:

// Proper error handling with context
class SafeProxy<T extends object> {
  static create<T extends object>(
    target: T,
    errorHandler?: (error: Error, operation: string, prop: string | symbol) => void
  ): T {
    return new Proxy(target, {
      get(obj, prop) {
        try {
          const value = obj[prop as keyof T];
          
          // โœ… Type checking
          if (typeof value === 'function') {
            return value.bind(obj);
          }
          
          return value;
        } catch (error) {
          // โœ… Provide context for debugging
          const enhancedError = new Error(
            `โŒ Error accessing property ${String(prop)}: ${error.message}`
          );
          
          if (errorHandler) {
            errorHandler(enhancedError, 'get', prop);
          } else {
            throw enhancedError;
          }
        }
      },
      
      set(obj, prop, value) {
        try {
          // โœ… Validation before setting
          if (value === undefined) {
            console.warn(`โš ๏ธ Setting ${String(prop)} to undefined`);
          }
          
          obj[prop as keyof T] = value;
          return true;
        } catch (error) {
          // โœ… Detailed error information
          const enhancedError = new Error(
            `โŒ Error setting property ${String(prop)}: ${error.message}`
          );
          
          if (errorHandler) {
            errorHandler(enhancedError, 'set', prop);
          } else {
            throw enhancedError;
          }
          
          return false;
        }
      }
    });
  }
}

// โœ… Usage with error handling
const config = SafeProxy.create(
  { apiUrl: 'https://api.example.com' },
  (error, operation, prop) => {
    console.error(`๐Ÿšจ Proxy error during ${operation} on ${String(prop)}:`, error);
  }
);

๐Ÿ› ๏ธ Best Practices

Follow these guidelines to master the Proxy Pattern! ๐ŸŒŸ

1. Choose the Right Proxy Type ๐ŸŽฏ

// ๐ŸŽฏ Decision helper for proxy selection
class ProxySelector {
  static recommendProxy(requirements: {
    needsLazyLoading?: boolean;
    needsAccessControl?: boolean;
    needsCaching?: boolean;
    needsLogging?: boolean;
    needsValidation?: boolean;
  }): string[] {
    const recommendations: string[] = [];

    if (requirements.needsLazyLoading) {
      recommendations.push('๐ŸŒ™ Virtual Proxy - Delays expensive operations');
    }
    if (requirements.needsAccessControl) {
      recommendations.push('๐Ÿ” Protection Proxy - Controls permissions');
    }
    if (requirements.needsCaching) {
      recommendations.push('๐Ÿ’พ Caching Proxy - Improves performance');
    }
    if (requirements.needsLogging) {
      recommendations.push('๐Ÿ“ Logging Proxy - Tracks operations');
    }
    if (requirements.needsValidation) {
      recommendations.push('โœ… Validation Proxy - Ensures data integrity');
    }

    return recommendations;
  }
}

// Example usage
const needs = {
  needsCaching: true,
  needsLogging: true,
  needsValidation: true
};

console.log('Recommended proxies:', ProxySelector.recommendProxy(needs));

2. Implement Transparent Proxies ๐Ÿ”

// ๐ŸŽฏ Transparent proxy that preserves object behavior
interface ProxyOptions {
  beforeGet?: (prop: string | symbol) => void;
  afterGet?: (prop: string | symbol, value: unknown) => void;
  beforeSet?: (prop: string | symbol, value: unknown) => boolean;
  afterSet?: (prop: string | symbol, value: unknown) => void;
}

function createTransparentProxy<T extends object>(
  target: T,
  options: ProxyOptions = {}
): T {
  return new Proxy(target, {
    get(obj, prop, receiver) {
      options.beforeGet?.(prop);
      
      // โœ… Preserve correct 'this' binding
      const value = Reflect.get(obj, prop, receiver);
      
      options.afterGet?.(prop, value);
      
      // โœ… Bind methods to maintain context
      if (typeof value === 'function') {
        return value.bind(obj);
      }
      
      return value;
    },
    
    set(obj, prop, value, receiver) {
      const shouldProceed = options.beforeSet?.(prop, value) ?? true;
      
      if (!shouldProceed) return false;
      
      // โœ… Use Reflect for proper behavior
      const result = Reflect.set(obj, prop, value, receiver);
      
      if (result) {
        options.afterSet?.(prop, value);
      }
      
      return result;
    },
    
    // โœ… Implement other traps for full transparency
    has(obj, prop) {
      return Reflect.has(obj, prop);
    },
    
    deleteProperty(obj, prop) {
      return Reflect.deleteProperty(obj, prop);
    }
  });
}

3. Document Proxy Behavior ๐Ÿ“š

// ๐ŸŽฏ Self-documenting proxy with metadata
class DocumentedProxy<T extends object> {
  private static metadata = new WeakMap<object, {
    type: string;
    purpose: string;
    behaviors: string[];
    created: Date;
  }>();

  static create<T extends object>(
    target: T,
    type: string,
    purpose: string,
    behaviors: string[]
  ): T {
    const proxy = new Proxy(target, {
      get(obj, prop) {
        if (prop === Symbol.for('proxy.metadata')) {
          return DocumentedProxy.metadata.get(proxy);
        }
        return obj[prop as keyof T];
      }
    });

    // โœ… Store metadata
    this.metadata.set(proxy, {
      type,
      purpose,
      behaviors,
      created: new Date()
    });

    return proxy;
  }

  static getInfo(proxy: any): string {
    const metadata = proxy[Symbol.for('proxy.metadata')];
    if (!metadata) return 'Not a documented proxy';

    return `
๐ŸŽฏ Proxy Information:
- Type: ${metadata.type}
- Purpose: ${metadata.purpose}
- Behaviors: ${metadata.behaviors.join(', ')}
- Created: ${metadata.created.toISOString()}
    `.trim();
  }
}

// โœ… Usage
const apiClient = { endpoint: 'https://api.example.com' };
const proxiedClient = DocumentedProxy.create(
  apiClient,
  'Caching Proxy',
  'Reduces API calls by caching responses',
  ['caching', 'logging', 'retry']
);

console.log(DocumentedProxy.getInfo(proxiedClient));

4. Test Proxy Behavior ๐Ÿงช

// ๐ŸŽฏ Proxy testing utilities
class ProxyTester {
  static testAccessControl<T extends object>(
    createProxy: (target: T, role: string) => T,
    target: T,
    tests: { role: string; operation: () => void; shouldSucceed: boolean }[]
  ): void {
    console.log('๐Ÿงช Testing access control proxy...');
    
    tests.forEach(({ role, operation, shouldSucceed }) => {
      const proxy = createProxy(target, role);
      
      try {
        operation.call(proxy);
        if (shouldSucceed) {
          console.log(`โœ… ${role}: Operation succeeded as expected`);
        } else {
          console.log(`โŒ ${role}: Operation should have failed!`);
        }
      } catch (error) {
        if (!shouldSucceed) {
          console.log(`โœ… ${role}: Operation failed as expected`);
        } else {
          console.log(`โŒ ${role}: Operation should have succeeded!`);
        }
      }
    });
  }
}

5. Performance Monitoring ๐Ÿ“Š

// ๐ŸŽฏ Performance-aware proxy
class PerformanceProxy<T extends object> {
  private metrics = new Map<string | symbol, {
    calls: number;
    totalTime: number;
    avgTime: number;
  }>();

  create(target: T): T {
    return new Proxy(target, {
      get: (obj, prop) => {
        const value = obj[prop as keyof T];
        
        if (typeof value === 'function') {
          return (...args: any[]) => {
            const start = performance.now();
            const result = value.apply(obj, args);
            const duration = performance.now() - start;
            
            // โœ… Track metrics
            this.updateMetrics(prop, duration);
            
            return result;
          };
        }
        
        return value;
      }
    });
  }

  private updateMetrics(method: string | symbol, duration: number): void {
    const current = this.metrics.get(method) || { calls: 0, totalTime: 0, avgTime: 0 };
    
    current.calls++;
    current.totalTime += duration;
    current.avgTime = current.totalTime / current.calls;
    
    this.metrics.set(method, current);
  }

  getReport(): string {
    let report = '๐Ÿ“Š Performance Report:\n';
    
    this.metrics.forEach((metrics, method) => {
      report += `
๐Ÿ“Œ ${String(method)}:
  - Calls: ${metrics.calls}
  - Avg Time: ${metrics.avgTime.toFixed(2)}ms
  - Total Time: ${metrics.totalTime.toFixed(2)}ms
      `.trim() + '\n';
    });
    
    return report;
  }
}

๐Ÿงช Hands-On Exercise

Time to put your Proxy Pattern skills to the test! ๐Ÿ’ช

Challenge: Build a Smart Shopping Cart ๐Ÿ›’

Create a shopping cart system with the following features:

  1. Validation: Ensure only valid products can be added
  2. Logging: Track all cart operations
  3. Caching: Cache total calculations
  4. Access Control: Different permissions for customers vs admins
// ๐ŸŽฏ Your challenge starts here!
interface Product {
  id: string;
  name: string;
  price: number;
  stock: number;
}

interface CartItem {
  product: Product;
  quantity: number;
}

interface ShoppingCart {
  items: CartItem[];
  addItem(product: Product, quantity: number): void;
  removeItem(productId: string): void;
  getTotal(): number;
  checkout(): void;
}

// TODO: Implement these proxy features:
// 1. ValidationProxy - Validate products and quantities
// 2. LoggingProxy - Log all operations
// 3. CachingProxy - Cache total calculations
// 4. AccessControlProxy - Control who can checkout

// Start coding here! ๐Ÿš€
๐Ÿ’ก Click here for the solution
// ๐ŸŽฏ Complete solution for Smart Shopping Cart

// Basic shopping cart implementation
class BasicShoppingCart implements ShoppingCart {
  items: CartItem[] = [];

  addItem(product: Product, quantity: number): void {
    const existing = this.items.find(item => item.product.id === product.id);
    
    if (existing) {
      existing.quantity += quantity;
    } else {
      this.items.push({ product, quantity });
    }
  }

  removeItem(productId: string): void {
    this.items = this.items.filter(item => item.product.id !== productId);
  }

  getTotal(): number {
    return this.items.reduce(
      (total, item) => total + (item.product.price * item.quantity),
      0
    );
  }

  checkout(): void {
    console.log('๐Ÿ’ณ Processing checkout...');
    console.log(`Total: $${this.getTotal().toFixed(2)}`);
    this.items = [];
  }
}

// 1. Validation Proxy
class ValidatedCart implements ShoppingCart {
  constructor(private cart: ShoppingCart) {}

  get items() { return this.cart.items; }

  addItem(product: Product, quantity: number): void {
    // โœ… Validate product
    if (!product.id || !product.name || product.price <= 0) {
      throw new Error('โŒ Invalid product!');
    }
    
    // โœ… Validate quantity
    if (quantity <= 0 || quantity > product.stock) {
      throw new Error(`โŒ Invalid quantity! Stock available: ${product.stock}`);
    }
    
    console.log('โœ… Product validation passed');
    this.cart.addItem(product, quantity);
  }

  removeItem(productId: string): void {
    if (!productId) {
      throw new Error('โŒ Product ID required!');
    }
    this.cart.removeItem(productId);
  }

  getTotal(): number {
    return this.cart.getTotal();
  }

  checkout(): void {
    if (this.items.length === 0) {
      throw new Error('โŒ Cart is empty!');
    }
    this.cart.checkout();
  }
}

// 2. Logging Proxy
class LoggedCart implements ShoppingCart {
  private operations: string[] = [];

  constructor(private cart: ShoppingCart) {}

  get items() { return this.cart.items; }

  addItem(product: Product, quantity: number): void {
    const log = `๐Ÿ“ Added ${quantity}x ${product.name} to cart`;
    this.operations.push(log);
    console.log(log);
    this.cart.addItem(product, quantity);
  }

  removeItem(productId: string): void {
    const log = `๐Ÿ“ Removed product ${productId} from cart`;
    this.operations.push(log);
    console.log(log);
    this.cart.removeItem(productId);
  }

  getTotal(): number {
    console.log('๐Ÿ“ Calculated total');
    return this.cart.getTotal();
  }

  checkout(): void {
    const log = `๐Ÿ“ Checkout completed at ${new Date().toISOString()}`;
    this.operations.push(log);
    console.log(log);
    this.cart.checkout();
  }

  getLog(): string[] {
    return [...this.operations];
  }
}

// 3. Caching Proxy
class CachedCart implements ShoppingCart {
  private totalCache: { value: number; dirty: boolean } = { value: 0, dirty: true };

  constructor(private cart: ShoppingCart) {}

  get items() { return this.cart.items; }

  addItem(product: Product, quantity: number): void {
    this.totalCache.dirty = true;
    this.cart.addItem(product, quantity);
  }

  removeItem(productId: string): void {
    this.totalCache.dirty = true;
    this.cart.removeItem(productId);
  }

  getTotal(): number {
    if (!this.totalCache.dirty) {
      console.log('๐Ÿ’พ Returning cached total');
      return this.totalCache.value;
    }
    
    console.log('๐Ÿ”„ Calculating fresh total');
    this.totalCache.value = this.cart.getTotal();
    this.totalCache.dirty = false;
    return this.totalCache.value;
  }

  checkout(): void {
    this.totalCache.dirty = true;
    this.cart.checkout();
  }
}

// 4. Access Control Proxy
type UserRole = 'customer' | 'admin';

class SecureCart implements ShoppingCart {
  constructor(
    private cart: ShoppingCart,
    private userRole: UserRole
  ) {}

  get items() { 
    if (this.userRole === 'admin') {
      return this.cart.items;
    }
    // Customers see limited info
    return this.cart.items.map(item => ({
      product: { ...item.product, stock: 0 }, // Hide stock
      quantity: item.quantity
    }));
  }

  addItem(product: Product, quantity: number): void {
    console.log(`๐Ÿ‘ค ${this.userRole} adding item`);
    this.cart.addItem(product, quantity);
  }

  removeItem(productId: string): void {
    console.log(`๐Ÿ‘ค ${this.userRole} removing item`);
    this.cart.removeItem(productId);
  }

  getTotal(): number {
    return this.cart.getTotal();
  }

  checkout(): void {
    if (this.userRole !== 'customer') {
      throw new Error('โŒ Only customers can checkout!');
    }
    console.log('โœ… Customer checkout authorized');
    this.cart.checkout();
  }
}

// ๐ŸŽฎ Putting it all together!
function createSmartCart(userRole: UserRole): ShoppingCart {
  const basicCart = new BasicShoppingCart();
  
  // Apply proxies in order
  const cachedCart = new CachedCart(basicCart);
  const loggedCart = new LoggedCart(cachedCart);
  const validatedCart = new ValidatedCart(loggedCart);
  const secureCart = new SecureCart(validatedCart, userRole);
  
  return secureCart;
}

// ๐Ÿงช Test the smart cart
const customerCart = createSmartCart('customer');
const adminCart = createSmartCart('admin');

// Test products
const laptop: Product = { id: '1', name: 'Gaming Laptop ๐Ÿ’ป', price: 1299.99, stock: 5 };
const mouse: Product = { id: '2', name: 'RGB Mouse ๐Ÿ–ฑ๏ธ', price: 79.99, stock: 20 };

// Customer operations
console.log('=== Customer Cart Test ===');
customerCart.addItem(laptop, 1);
customerCart.addItem(mouse, 2);
console.log(`Total: $${customerCart.getTotal()}`); // Cached on second call
console.log(`Total: $${customerCart.getTotal()}`); // From cache!
customerCart.checkout(); // โœ… Success

// Admin operations
console.log('\n=== Admin Cart Test ===');
adminCart.addItem(laptop, 1);
try {
  adminCart.checkout(); // โŒ Admins can't checkout
} catch (error) {
  console.log(error.message);
}

// Invalid operations
console.log('\n=== Validation Test ===');
try {
  customerCart.addItem({ id: '', name: '', price: -10, stock: 0 }, 1);
} catch (error) {
  console.log(error.message); // โŒ Invalid product
}

console.log('\n๐ŸŽ‰ Smart cart with multiple proxy layers working perfectly!');

๐ŸŽ“ Key Takeaways

Congratulations! Youโ€™ve mastered the Proxy Pattern in TypeScript! ๐ŸŽ‰ Hereโ€™s what youโ€™ve learned:

  • ๐ŸŽฏ Proxy Pattern Fundamentals: Understanding how proxies control access to objects
  • ๐Ÿ›ก๏ธ Different Proxy Types: Virtual, Protection, Remote, Caching, and Logging proxies
  • ๐Ÿ’ป TypeScript Implementation: Using both custom classes and JavaScriptโ€™s Proxy object
  • ๐Ÿš€ Advanced Techniques: Proxy chains, revocable proxies, and dynamic validation
  • โš ๏ธ Common Pitfalls: Performance overhead, identity issues, and error handling
  • ๐Ÿ› ๏ธ Best Practices: Choosing the right proxy type and maintaining transparency

The Proxy Pattern is like having a smart assistant that manages access to your important resources! ๐Ÿค

๐Ÿค Next Steps

Ready to continue your TypeScript design patterns journey? Hereโ€™s what to do next:

  1. ๐Ÿ”จ Practice: Implement different proxy types in your current projects
  2. ๐Ÿ” Explore: Combine proxies with other patterns like Decorator or Adapter
  3. ๐Ÿ“š Learn More: Check out the Flyweight Pattern for memory optimization
  4. ๐Ÿš€ Next Tutorial: Continue with [Flyweight Pattern: Memory Optimization]

Keep coding, keep learning, and remember - with great proxy power comes great responsibility! ๐Ÿฆธโ€โ™‚๏ธโœจ

Happy proxying! ๐ŸŽฏ๐Ÿš€