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…