3

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?

customcommander
  • 17,580
  • 5
  • 58
  • 84

2 Answers2

1

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.

jirassimok
  • 3,850
  • 2
  • 14
  • 23
0

An interesting option is to work with Promises. Comlink has an interesting usage of this.

Expanding on jirassimok's answer, if everything is async, then the termination token becomes "then".

function safe_access(value) {
    let obj = (typeof value === 'object') ? value : {};

    return new Proxy(obj, {
        value: value,
        get: function(target, property) {
            if (property === "then")
                return this.value;
            else
                return safe_access(target[property]);
        }
    });
}

async function main() {
  let a = safe_access({'x': {'y': 123}});
  // a.x.y is a proxy object
  (await a.x.y) === 123
  (await a.x.y.z) === undefined
}

main();

This wasn't immediately obvious to me, as I've been using async/await for a long time now.