Skip to content
Version

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: If true, nested objects are also proxied automatically.
  • options.watch: Restrict hooks to only these property names.

Returns

  • A Proxy for the given object with the same type as item.

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 → Bob

Transform 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 intercepted

Deep 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"

See Also

  • diff: Compare two plain objects, returning their structural differences.
  • prune: Remove null/empty values from an object.