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 ๐ง
- Subject Interface: Defines the common interface for RealSubject and Proxy
- RealSubject: The actual object that the proxy represents
- Proxy: Maintains a reference to the RealSubject and controls access to it
- 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:
- Validation: Ensure only valid products can be added
- Logging: Track all cart operations
- Caching: Cache total calculations
- 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:
- ๐จ Practice: Implement different proxy types in your current projects
- ๐ Explore: Combine proxies with other patterns like Decorator or Adapter
- ๐ Learn More: Check out the Flyweight Pattern for memory optimization
- ๐ Next Tutorial: Continue with [Flyweight Pattern: Memory Optimization]
Keep coding, keep learning, and remember - with great proxy power comes great responsibility! ๐ฆธโโ๏ธโจ
Happy proxying! ๐ฏ๐