proxy
The proxy utility wraps an object in a JavaScript Proxy that intercepts property get and set operations, allowing you to observe or transform property access without modifying the original object.
Source Code
View Source Code
ts
import type { Obj } from '../types';
import { isObject } from '../typed/isObject';
// #region ProxyOptions
type ProxyOptions<T> = {
deep?: boolean;
get?: <K extends PropertyKey>(prop: K, val: unknown, target: T) => unknown;
set?: <K extends PropertyKey>(prop: K, curr: unknown, prev: unknown, target: T) => unknown;
watch?: (keyof T)[];
};
// #endregion ProxyOptions
/**
* Creates a new Proxy for the given object that invokes functions when properties are accessed or modified.
*
* @example
* ```ts
* const obj = { a: 1, b: 2 };
* const log = (prop, curr, prev, target) => console.log(`Property '${prop}' changed from ${prev} to ${curr}`);
* const proxyObj = proxy(obj, { set: log });
* proxyObj.a = 3; // logs 'Property 'a' changed from 1 to 3'
* ```
*
* @param item - The object to observe.
* @param options - Configuration options for the proxy.
* @param [options.set] - A function to call when a property is set.
* @param [options.get] - A function to call when a property is accessed.
* @param [options.deep] - If true, the proxy will also apply to nested objects.
* @param [options.watch] - An array of property names to watch.
*
* @returns A new Proxy for the given object.
*/
export function proxy<T extends Obj>(item: T, options: ProxyOptions<T>): T {
const { deep = false, get, set, watch } = options;
const watchSet = watch ? new Set<PropertyKey>(watch as PropertyKey[]) : null;
const handler: ProxyHandler<T> = {
get(target, prop, receiver) {
if (watchSet && !watchSet.has(prop)) {
return Reflect.get(target, prop, receiver);
}
let value = Reflect.get(target, prop, receiver);
if (get) {
value = get(prop, value, target) as any;
}
if (deep && isObject(value)) {
return proxy(value as unknown as T, options);
}
return value;
},
set(target, prop, val, receiver) {
if (watchSet && !watchSet.has(prop)) {
return Reflect.set(target, prop, val, receiver);
}
const prev = target[prop as keyof T];
const value = set ? set(prop, val, prev, target) : val;
if (deep && isObject(value)) {
return Reflect.set(target, prop, proxy(value as unknown as T, options), receiver);
}
return Reflect.set(target, prop, value, receiver);
},
};
return new Proxy(item, handler);
}Features
- Non-destructive: The original object is not modified.
- Get & Set hooks: Intercept both reads and writes independently.
- Deep mode: Automatically wraps nested objects too.
- Watch list: Limit interception to specific property names.
API
ts
function proxy<T extends object>(item: T, options: ProxyOptions<T>): T;
type ProxyOptions<T> = {
set?: <K extends PropertyKey>(prop: K, curr: unknown, prev: unknown, target: T) => unknown;
get?: <K extends PropertyKey>(prop: K, val: unknown, target: T) => unknown;
deep?: boolean;
watch?: (keyof T)[];
};Parameters
item: The object to wrap.options.set: Called when a property is set. The return value becomes the stored value.options.get: Called when a property is accessed. The return value is what the caller receives.options.deep: Iftrue, nested objects are also proxied automatically.options.watch: Restrict hooks to only these property names.
Returns
- A
Proxyfor the given object with the same type asitem.
Examples
Observe Property Changes
ts
import { proxy } from '@vielzeug/toolkit';
const state = { count: 0, name: 'Alice' };
const observed = proxy(state, {
set: (prop, curr, prev) => {
console.log(`${String(prop)}: ${prev} → ${curr}`);
return curr;
},
});
observed.count = 5; // logs: count: 0 → 5
observed.name = 'Bob'; // logs: name: Alice → BobTransform on Get
ts
import { proxy } from '@vielzeug/toolkit';
const config = { apiUrl: 'https://api.example.com', timeout: 5000 };
const secured = proxy(config, {
get: (prop, val) => {
if (prop === 'apiUrl') return val; // allow
return '***'; // mask everything else
},
});
secured.apiUrl; // 'https://api.example.com'
secured.timeout; // '***'Watch Specific Keys Only
ts
import { proxy } from '@vielzeug/toolkit';
const user = { id: 1, name: 'Alice', role: 'admin' };
const changes: string[] = [];
const watched = proxy(user, {
set: (prop, curr) => {
changes.push(String(prop));
return curr;
},
watch: ['name'], // only 'name' is intercepted
});
watched.name = 'Bob'; // changes → ['name']
watched.role = 'user'; // not interceptedDeep Proxy
ts
import { proxy } from '@vielzeug/toolkit';
const data = { user: { profile: { theme: 'dark' } } };
const deep = proxy(data, {
set: (prop, curr, prev) => {
console.log(`${String(prop)}: ${JSON.stringify(prev)} → ${JSON.stringify(curr)}`);
return curr;
},
deep: true,
});
deep.user.profile.theme = 'light'; // logs: theme: "dark" → "light"