12/08/2018, 16:41

Accessing deeply nested object property

Anything that can go wrong will go wrong So we adds checks everywhere to make sure the data comes from outside is in the correct shape. Soon, we will realize that we've created this monster. Looking for a value nested deeply inside an object is very popular in Javascript codes and you ...

Anything that can go wrong will go wrong

So we adds checks everywhere to make sure the data comes from outside is in the correct shape. Soon, we will realize that we've created this monster.

Looking for a value nested deeply inside an object is very popular in Javascript codes and you usually find yourself doing tons of checks for null/undefined values. Like this

if (a && a.b) {
    console.log(a.b.c);
}

Or if somehow you want to look smart and go for an one-liner like this

const b = a ? a.b : null;
const c = a.b && a.b.c;

Imagine it's 4-5 level nested. That would be a nightmare to read. So here are a few ways one might find useful to do that in a more readable way. Each of them has their own advantages and drawbacks. And all of them allow you to do it without any if statement.

Wrap in try/catch block

This is a super simple yet super effective approach. You just simply catch the error and safely return null or any fallback value. Of course you would make a function for it to reuse anywhere. Here's a simple one.

function get(obj, accessor) {
    try {
        return accessor(obj)
    } catch (error) {
        if (error instanceof TypeError) {
            return null
        }

        throw error
    }
}

Here's how we'd use it

const obj = {
    a: {
        b: {
            c: 'c'
        }
    }
};

const val = get(obj, _ => _.a.b.c)

If you use flow, you can still get all the type-checking working properly with this method. You can have more complex logic in the accessor function such as a conditional path and everything is wrapped and safely executed.

Using a string path

Popular JavaScript libraries usually support this convenient function that allows us to specify the path to the value that we expect in a string. If null or undefined is encountered anywhere in the path, the function will safely terminate and give a default value of choice. It's really simple and widely available.

It may look very similar to the former one, but its implementation is not as simple and probaly not as effective since it involves parsing the string and usually a loop to traverse the nested object. But it most likely won't have any notable impact anyway. And since it uses string, you won't get helpful information from compile-time type-checking (e.g. flow). But it won't matter if your object doesn't have a known structure or if you just don't use flow at all. In that case, a string path might be easier to read. Overall, it comes to your syntax preference to choose either one to use.

Here's an example with lodash

const obj = {
    a: {
        b: {
            c: 'c'
        }
    }
};

console.log(_.get(obj, 'a.b.c')); // 'c'
console.log(_.get(obj, 'a.c.d')); // null

The Maybe monad

Since functional programming is raising in popularity, here's a functional approach. It would be a lengthy post to explain what monad is and then what the maybe monad is, so let's just jump into a simple implementation instead.

class Maybe {
    constructor (value) {
        this.value = value;
    }

    static of(val) {
        return new Maybe(val);
    }

    prop(name) {
        return this.value && this.value[name]
            ? Maybe.of(this.value[name])
            : Maybe.of(null);
    }

    path(path) {
        return Maybe.of(_.get(this.value, path), Maybe.of(null));
    }

    orElse(val) {
        return this.value ? this : Maybe.of(val);
    }

    join() {
        return this.value;
    }
}

It's not really a monad though since it lacks some methods, but anyway, just for demonstration. Here's how you would use it.

const obj = {
    a: {
        b: {
            c: 'c'
        }
    }
};

Maybe.of(obj).path('a.b.c').join()

Now it may look uglier or nicer depends on how much you like the functional programming paradigm. It still has the drawback of not benefiting from compile-time type-checking but as mentioned before, you may not care about it at all anyway. You have to write more codes than before, but it's written in a more decrative style. You can write something like this

Maybe.of(obj)
    .prop('a')
    .orElse(otherObj)
    .path('b.c')
    .orElse('Nothing found')
    .join()

Optional chain operator

All of those methods requires some extra code/library. How about a method that is natively supported. It's the Optional chain operator.

Actually it is not included in ECMAScript yet, it's still a proposal at stage 1. There's already a babel plugin that you can try now. Similar features are implemented in other popular languages like C#, Swift or Ruby. So basically, you use a question mark to mark for unsure (optional) property that you want to "defend" against.

Here's your example (There are more in its proposal page)

a?.b    // undefined if `a` is null/undefined, `a.b` otherwise.
a?.[x]  // undefined if `a` is null/undefined, `a.[x]` otherwise.
a?.b.c  // TypeError will be thrown if b is null or undefined

There may be times when you may find the former methods causing issues that are annoying to debug. Because they hide the error and just safely return a fallback value. It may take time to figure out where it breaks. This optional chaining feature allows you to specify only which properties you actually want to be optional. So when other properties that should exist turn out to be null/undefined, you can handle the exception accordingly. However, it would require you putting question mark everywhere and if you just simply want to get the value and don't care about the exception, it might be annoying.

And hopefully, flow will support this if this proposal is added in a future ES version.

0