Skip to content
VersionSize

proxy

The proxy utility creates an enhanced JavaScript Proxy for an object, allowing you to intercept and react to property access (get) and modifications (set). It features support for selective property watching and optional deep proxying for nested structures.

Implementation

View Source Code
ts
import { isObject } from '../typed/isObject';
import type { Obj } from '../types';

// #region ProxyOptions
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)[];
};
// #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. It receives the property name, current value, previous value, and the target object.
 * @param [options.get] - A function to call when a property is accessed. It receives the property name, value, and the target object.
 * @param [options.deep] - If true, the proxy will also apply to nested objects.
 * @param [options.watch] - An array of property names to watch. If provided, only these properties will trigger the set and get functions.
 *
 * @returns A new Proxy for the given object.
 */
export function proxy<T extends Obj>(item: T, options: ProxyOptions<T>): T {
  const { set, get, deep = false, watch } = options;

  const handler: ProxyHandler<T> = {
    get(target, prop, receiver) {
      if (watch && !watch.includes(prop as keyof T)) {
        return Reflect.get(target, prop, receiver);
      }

      let value = Reflect.get(target, prop, receiver);

      if (get) {
        // biome-ignore lint/suspicious/noExplicitAny: -
        value = get(prop, value, target) as any;
      }

      if (deep && isObject(value)) {
        return proxy(value as T[keyof T], options);
      }

      return value;
    },
    set(target, prop, val, receiver) {
      if (watch && !watch.includes(prop as keyof T)) {
        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 T[keyof T], options), receiver);
      }

      return Reflect.set(target, prop, value, receiver);
    },
  };

  return new Proxy(item, handler);
}

Features

  • Isomorphic: Works in both Browser and Node.js.
  • Reactive Handlers: Execute custom logic whenever properties are read or updated.
  • Deep Proxying: Optionally intercept changes in nested objects and arrays automatically.
  • Selective Watching: Limit interceptions to a specific list of keys for better performance.
  • Type-safe: Preserves the original object's interface.

API

Type Definitions
ts
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)[];
};
ts
function proxy<T extends object>(item: T, options?: ProxyOptions<T>): T;

Parameters

  • item: The source object to wrap in a Proxy.
  • options: Optional configuration:
    • set: Callback triggered when a property value is changed.
    • get: Callback triggered when a property value is accessed.
    • deep: If true, nested objects are also wrapped in proxies (defaults to false).
    • watch: An array of keys to monitor. If provided, callbacks only trigger for these properties.

Returns

  • A new Proxy object that behaves like the original but triggers the specified handlers.

Examples

Watching Property Changes

ts
import { proxy } from '@vielzeug/toolkit';

const user = { name: 'Alice', age: 25 };

const observableUser = proxy(user, {
  set: (prop, next, prev) => {
    console.log(`${String(prop)} changed from ${prev} to ${next}`);
  },
});

observableUser.name = 'Bob'; // Logs: name changed from Alice to Bob

Selective Watching (Deep)

ts
import { proxy } from '@vielzeug/toolkit';

const config = {
  api: { host: 'localhost' },
  ui: { theme: 'dark' },
};

// Only watch the 'api' key, and do it deeply
const watchedConfig = proxy(config, {
  deep: true,
  watch: ['api'],
  set: (prop) => console.log(`API config updated: ${String(prop)}`),
});

watchedConfig.api.host = 'api.example.com'; // Triggers callback
watchedConfig.ui.theme = 'light'; // Does NOT trigger callback

Implementation Notes

  • Performance-optimized to avoid overhead on un-watched properties.
  • Returns a standard Proxy object that can be used anywhere the original object is accepted.
  • Throws TypeError if item is not a proxy-able object (e.g., a primitive).

See Also

  • memo: Cache results of function calls.
  • path: Safely access nested data.
  • merge: Combine multiple objects.