TypeScript icon, indicating that this package has built-in type declarations

    2.0.2 • Public • Published


    Deep proxy implementation for TypeScript

    Build Status Maintainability Test Coverage npm (scoped)


    # npm
    npm i @qiwi/deep-proxy
    # yarn
    yarn add @qiwi/deep-proxy

    Key features

    • Single proxy handler with rich context instead of verbose traps mapping
    • Proxy self-reference in handler context (meaningful for methods binding)
    • JS, TS and Flow support
    • Magic directives:
      • PROXY to provide infinitely nested proxies
      • DEFAULT to provide default behavior
    • Caching
    • Trap params parsing


    import {DeepProxy} from '@qiwi/deep-proxy'
    const target = {foo: 'bar', a: {b: 'c'}}
    const proxy = new DeepProxy(target, ({trapName, value, key, DEFAULT, PROXY}: THandlerContext) => {
      if (trapName === 'set') {
        throw new TypeError('target is immutable')
      if (trapName === 'get') {
        if (typeof value === 'object' && value !== null) {
          return PROXY
        if (key === 'd') {
          return 'baz'
        return 'qux'
      return DEFAULT
    proxy.foo       // qux
    proxy.a.b       // qux
    proxy.bar       // qux
    proxy.d         // baz
    proxy.a = 'a'   // TypeError

    FP adepts may use createDeepProxy factory instead of DeepProxy class and get some magic.

    import {createDeepProxy} from '@qiwi/deep-proxy'
    // Regular usage case
    const handler = ({DEFAULT}) => DEFAULT
    const proxy1 = createDeepProxy(target, handler)
    // Passing defaults through this context
    const customProxyFactory = createDeepProxy.bind({handler})
    const proxy2 = customProxyFactory(target)

    All the traps follow to the single handler, so you're able to build various complex conditions in one place. This approach might be useful if you need some kind of "rich" model, but you don't want to complicate DTO.

    if (path.length > 10) {
      if (prop === 'foo' || ['get', 'set', 'ownKeys'].includes(trapName)) {
        return DEFAULT
      throw new Error('Bla-bla')
    if (trapName === 'set' && typeof value !== 'number') {
      throw new TypeError('only `number` type is allowed')
    // and so on
    return DEFAULT

    Another example. Imagine a client which uses an unstable channel with 10% of loss.

    const client = createClient({...opts})
    await client.foo(...args) // 90% of success
    const clientWithRetry = new DeepProxy(client, ({value, trapName}: THandlerContext) => {
      if (trapName === 'get') {
        if (typeof value === 'function') {
          return retryify(value, 2)
        if (typeof value === 'object' && value !== null) {
          return PROXY
      return DEFAULT
    await clientWithRetry.foo(...args) // 99% of success

    Metrics, debugging, throttling — all becomes better with the deep proxy.





    Directive Description
    DEFAULT Returns standard flow control. The current operation (get, set, ownKeys, etc) will be performed as without proxy.
    PROXY Returns a proxy of nested object with parent's proxy handler.

    A bit more sugar on top: by default PROXY directive uses value from context, but you can pass your own.

    const proxy = new DeepProxy({foo: {bar: 'baz'}}, ({value, trapName}) => {
      if (trapName === 'get' && typeof value === 'object' && value !== null) {
        return PROXY({baz: 'qux'})
      return DEFAULT
    proxy.foo.baz // 'qux'


    type THandlerContext<T extends TTarget> = {
      target: T               // proxy target object/function
      parameters: any[]       // trap arguments as is: [target, *, *, *]
      name: keyof T,          // property name
      val: any,               // property value
      receiver: any,          // receiver ('get' or 'set' traps)
      args: any[],            // arguments array ('apply' trap)
      descriptor: PropertyDescriptor,   // property descriptor ('defineProperty', 'deleteProperty' trap)
      thisValue: any,         // this context of 'apply' trap
      prototype: any,         // prototype of 'setPrototypeOf' trap
      trapName: TTrapName     // proxy handler trap: get, set, ownKeys and so on
      traps: TTraps           // proxy handler map reference
      root: TTarget           // root level proxy's target
      path: string[]          // path to current proxy from root
      key?: keyof T           // prop key if defined in trap args
      value: any              // current field value by key
      newValue?: any          // new assigned value (#set())
      handler: TProxyHandler  // handler reference
      PROXY: symbol           // directives
      DEFAULT: symbol
      proxy: TTarget          // proxy reference


    createDeepProxy factory returns the stored proxy reference if the arguments match to any previous call:

    1. target refs are strictly equal
    2. root refs equal too
    3. path values match

    Note, this is not a regular memoization, but a loosely coupled WeakMap, so unused proxies can be cleaned up by GC.

    export type TProxyCache = {
      // root object refers to some targets objects,
      // that refer to map, that binds nested paths with their traps
      traps: WeakMap<TTarget, WeakMap<TTarget, Map<string, TTraps>>>
      // And these traps refer to proxies
      proxies: WeakMap<TTraps, TProxy<TTarget>>


    Proxies are slow. Very slow. Use them wisely with care.

    Alternatives & Refs




    npm i @qiwi/deep-proxy

    DownloadsWeekly Downloads






    Unpacked Size

    97.5 kB

    Total Files


    Last publish


    • antongolub
    • qiwibot
    • pismenskiy