Search code examples
javascriptes6-proxy

How to detect the end of a get sequence in a proxy object?


In order to avoid error when accessing deeply nested properties, I wrote a proxy-returning function:

const safe_access = obj =>
  new Proxy(obj, {
    get: (o, k) =>
      o[k] == null
        ? safe_access({})
        : typeof o[k] === 'object'
          ? safe_access(o[k])
          : o[k] });

Here's an example:

const a = safe_access({});
a.x.y.z; // no TypeError 🎉

However in its current form safe_access is unable to tell when it has reached the end of the path. Meaning that it cannot return undefined to signify that the property truly doesn't exist. This also means that you cannot have default values:

const a = safe_access({});
a.x.y.z || 42; // not 42

const {x: {y: {z = 42}}} = a;
z; // not 42

How can my proxy object detect the end of a property lookup?


Solution

  • This answer more or less applies here, for the same reasons.

    You can't detect the end of an access chain because nothing makes it any different from the preceding accesses. At runtime, the following code is effectively identical to let r = a.x.y.z.

    let r = a;
    {
        r = r.x;
        r = r.y;
        r = r.z
    }
    

    If you actually want to use this sort of safe navigation in code you're writing, the best is to use the optional chaining (?.) and nullish coalescing (??) operators added to the Javascript standard in 2020. They provide a neater, less confusing way to do this sort of thing, and are supported by all modern browsers.

    let r = a?.x?.y?.z ?? 42;
    

    If you need to support legacy browsers, you can get these operators from these two Babel plugins: ([1], [2]).


    However, if you really want to implement "safe" access yourself, there are a few tricks you can use to get around this.

    One trick, that probably requires the least additional work is to reserve one name to indicate the end of the safe access chain. (I've done something similar in Python in the past.)

    function safe_access(value) {
        let obj = (typeof(value) === 'object') ? value : {};
    
        return new Proxy(obj, {
            value: value,
            get: function(target, property) {
                if (property === "$")
                    return this.value;
                else
                    return safe_access(target[property]);
            }
        });
    }
    
    let a = {'x': {'y': 123}};
    // a.x.y is a proxy object
    a.x.y.$ === 123
    a.x.y.z.$ === undefined
    

    Another trick is detailed in this blog post.