iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Proxy

A Proxy stands in for another object, controlling access. Use it for lazy loading, caching, access control, logging, or remoting — anywhere you want to intercept calls without changing the real subject’s interface.

Lazy, caching, access, virtual proxies

EXAMPLE
// 1) Lazy-loading proxy — heavy object created only when first used
interface Document {
    render(): string;
}

class RealDocument implements Document {
    constructor(private path: string) {
        console.log(`Reading ${path} from disk (expensive)`);
        // imagine reading and parsing a 50 MB file here
    }
    render() { return `document contents from ${this.path}`; }
}

class LazyDocumentProxy implements Document {
    private real?: RealDocument;
    constructor(private path: string) {}
    render() {
        if (!this.real) this.real = new RealDocument(this.path);
        return this.real.render();
    }
}

const doc = new LazyDocumentProxy('/big/doc.pdf');   // nothing loaded yet
// later…
doc.render();                                          // loads on demand

// 2) Caching proxy — memoizes expensive calls
interface Translator {
    translate(text: string): Promise<string>;
}

class RemoteTranslator implements Translator {
    async translate(text: string) {
        const res = await fetch('https://api.example.com/t', { method: 'POST', body: text });
        return res.text();
    }
}

class CachingTranslatorProxy implements Translator {
    private cache = new Map<string, Promise<string>>();
    constructor(private inner: Translator) {}
    async translate(text: string) {
        if (!this.cache.has(text)) this.cache.set(text, this.inner.translate(text));
        return this.cache.get(text)!;
    }
}

const translator = new CachingTranslatorProxy(new RemoteTranslator());
// Identical inputs → one network call.

// 3) Access-control / protection proxy
interface BankAccount {
    withdraw(amount: number): void;
    balance(): number;
}

class RealBankAccount implements BankAccount {
    constructor(private _balance: number) {}
    withdraw(amount: number) { this._balance -= amount; }
    balance() { return this._balance; }
}

class BankAccountProxy implements BankAccount {
    constructor(private real: RealBankAccount, private user: { id: string; isOwner: boolean; isAdmin: boolean }) {}
    withdraw(amount: number) {
        if (!this.user.isOwner) throw new Error('Only the owner can withdraw');
        if (amount > 10_000 && !this.user.isAdmin) throw new Error('Limit exceeded');
        this.real.withdraw(amount);
    }
    balance() {
        if (!this.user.isOwner && !this.user.isAdmin) throw new Error('Forbidden');
        return this.real.balance();
    }
}

// 4) Logging / instrumentation proxy via JS Proxy
function loggingProxy<T extends object>(target: T, name: string): T {
    return new Proxy(target, {
        get(t, prop, recv) {
            const value = Reflect.get(t, prop, recv);
            if (typeof value === 'function') {
                return function (...args: unknown[]) {
                    console.log(`${name}.${String(prop)}(${args})`);
                    const out = value.apply(t, args);
                    console.log(`${name}.${String(prop)} = `, out);
                    return out;
                };
            }
            return value;
        },
    });
}

class Calculator { add(a: number, b: number) { return a + b; } }
const calc = loggingProxy(new Calculator(), 'calc');
calc.add(2, 3);   // logs the call and the result

// 5) Virtual proxy — placeholder for huge in-memory objects
class ImageProxy {
    private _full?: BigImage;
    constructor(private path: string, public width: number, public height: number) {}
    thumbnail() { return `tiny preview for ${this.path}`; }
    full(): BigImage {
        if (!this._full) this._full = new BigImage(this.path);
        return this._full;
    }
}

// 6) Remote proxy — local object stands in for a remote resource
class UserServiceProxy {
    async getUser(id: string) {
        const res = await fetch(`/api/users/${id}`);
        if (!res.ok) throw new Error('lookup failed');
        return res.json();
    }
}
// Callers use UserServiceProxy as if it were a local service.

// 7) Comparison
//   Proxy   — same interface, controls access
//   Decorator — same interface, adds behavior
//   Adapter — DIFFERENT interface, converts
//   Facade  — simplified interface to a complex subsystem

// 8) When to use proxy
//   • Object is expensive to create or maintain (lazy / virtual)
//   • You need to add caching, validation, or auth WITHOUT modifying the subject
//   • You're calling a remote service and want a local shape (remote proxy)
//   • You need transparent instrumentation (logging, metrics)

// 9) When NOT to use
//   • One-shot wrappers — just call the underlying method
//   • Cross-cutting concerns better solved by middleware (HTTP pipeline)
//   • When inheritance is clearer than composition for this domain

// 10) Common bugs
//   • Forgetting to forward all methods → silent missing behavior
//   • Caching mutable objects without copying → spooky action at a distance
//   • Holding the real subject forever → memory leak
//   • Access proxy doing the wrong check first → privilege escalation

Why it matters

A proxy keeps the same interface as the real subject — that’s what makes it transparent to callers. If you find yourself reaching for one to change the interface, you actually want an Adapter; to add behavior on top of the same interface, that’s a Decorator.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
function lazy(load) {
    let value, ready = false;
    return {
        get() { if (!ready) { value = load(); ready = true; } return value; },
    };
}
Try it Yourself »

Discussion

Loading…