DEV Community

Cover image for No Optional Chaining? No Problem. Write Your Own deepGet Function!
Nick Scialli (he/him)
Nick Scialli (he/him)

Posted on

No Optional Chaining? No Problem. Write Your Own deepGet Function!

Optional chaining is great: it allows us to safely access object properties without worrying if they exist:

const user: {
  name: "Bob"
}
const val = user?.pet?.name;
Enter fullscreen mode Exit fullscreen mode

Rather than crashing if pet doesn't exist on user, it'll simply return undefined.

While this is promising, optional chaining is in stage 4 of proposal for the ECMAScript standard, and is therefore not guaranteed to be available unless specifically accommodated in a transpiler you're using.

Rolling Your Own

The good news is we can fairly easily roll our own optional chaining-like function! Let's call it deepGet:

function deepGet(obj, ...props) {
  try {
    return props.reduce((acc, el) => acc[el], obj);
  } catch (e) {
    return undefined;
  }
}
Enter fullscreen mode Exit fullscreen mode

We can see that we try to iterate over all props in our reduce function. If an error is ever thrown, we just return undefined! Let's see it in action:

const obj = {
  user: {
    name: "Joe",
    age: 20,
    pets: [
      {
        name: "Daffodil",
        type: "dog",
        toys: [
          {
            name: "Toughy",
            price: 1999
          }
        ]
      }
    ]
  }
};

function deepGet(obj, ...props) {
  try {
    return props.reduce((acc, el) => acc[el], obj);
  } catch (e) {
    return undefined;
  }
}

console.log(deepGet(obj, "user", "pets", 0, "toys", 0, "price"));
// 1999
console.log(deepGet(obj, "user", "dogs", 0, "toys", 0, "price"));
// undefined
Enter fullscreen mode Exit fullscreen mode

And there you have it, your own safe deepGet function!

Discussion (13)

Collapse
dividedbynil profile image
Kane Ong

I know another cool trick, using Proxy.

function safeGet (obj) {
  return new Proxy({}, {
    get(_, prop) {
      if (prop == '_') // set whatever you like as the end
        return obj
      const next = typeof obj == 'undefined' ? obj : obj[prop]
      return safeGet(next)
    }
  })
}

const obj = {
  user: {
    name: "Joe",
    age: 20,
    pets: [
      {
        name: "Daffodil",
        type: "dog",
        toys: [
          {
            name: "Toughy",
            price: 1999
          }
        ]
      }
    ]
  }
};

console.log(safeGet(obj).user.pets[0].toys[0].price._)
// 1999
console.log(safeGet(obj).user.dogs[0].toys[0].price._)
// undefined

The syntax is very similar to native.

Collapse
lukeshiru profile image
LUKESHIRU

You could use ts-optchain, which provides a similar approach using Proxy, but the syntax is actually:

oc(object).foo.bar.foobar("default value");
// object?.foo?.bar?.foobar ?? "dafault value"
Collapse
dividedbynil profile image
Kane Ong

Nice one! Thanks for sharing!

Collapse
ecyrbe profile image
ecyrbe

This is far better than the one provided by OP . Thanks

Collapse
aminnairi profile image
Amin

Until you have a property named _ that you can't access anymore.

const obj = {
  user: {
    name: "Joe",
    age: 20,
    _: [
      {
        name: "Daffodil",
        type: "dog",
        toys: [
          {
            name: "Toughy",
            price: 1999
          }
        ]
      }
    ]
  }
};

console.log(safeGet(obj).user._.toys[0].price._);
// TypeError: Cannot read property '0' of undefined
Thread Thread
dividedbynil profile image
Kane Ong • Edited on

Thanks for pointing out, feel free to try this.

function safeGet (obj, end='_') {
  return new Proxy({}, {
    get(_, prop) {
      if (prop == end)
        return obj
      const next = typeof obj == 'undefined' ? obj : obj[prop]
      return safeGet(next, end)
    }
  })
}

const obj = {
  user: {
    name: "Joe",
    age: 20,
    _: [
      {
        name: "Daffodil",
        type: "dog",
        toys: [
          {
            name: "Toughy",
            price: 1999
          }
        ]
      }
    ]
  }
};

console.log(safeGet(obj, 'end').user._[0].toys[0].price.end); // 1999
console.log(safeGet(obj, 'end').user._.toys[0].price.end); // undefined
Thread Thread
aminnairi profile image
Amin

This API looks very sexy, don't get me wrong, but it has some flaws:

  • You would have to know 100% of all the properties of the object to search the properties from, this is not always possible, especially from large code-bases.
  • If you use an object from an untrusted source or an API, nothing guarantees that you won't get something different tomorrow, and this could crash your application if you are using the same end as one of the property of this API.
  • Even if you handle the JavaScript exception thrown from accessing a bad index, this totally blocks the user from getting the expected behavior, while the solution provided by the OP (and some other libraries like Lodash) would work as expected.

But if you are using an object from a trusted source, then your solution is very elegant.

Thread Thread
dividedbynil profile image
Kane Ong • Edited on

You would have to know 100% of all the properties of the object to search the properties from, this is not always possible, especially from large code-bases.

This claim is false, no matter the size of code-bases, you always have to know where to access the right field in that JSON. Knowing the JSON field location already avoided a bad ending.

If you use an object from an untrusted source or an API, nothing guarantees that you won't get something different tomorrow, and this could crash your application if you are using the same end as one of the properties(your typo) of this API.

The origin of the problem above comes from the untrusted source, not my provided solution. Optional chaining can't solve it either.

Reminder: All of my solutions do avoid crashing from accessing bad indices.

Even if you handle the JavaScript exception thrown from accessing a bad index, this totally blocks the user from getting the expected behavior, while the solution provided by the OP (and some other libraries like Lodash) would work as expected.

Optional chaining is the topic of this article, which means no exception is thrown from accessing a bad index. Have you tried to access a bad index from my solutions yet? It behaves as expected.

Collapse
aminnairi profile image
Amin

Hi Nick!

Awesome usage of the rest operator in this context, I really like this API. this looks more natural than using a string or an array for accessing a chain of properties.

For the return type, I would have returned null instead of undefined. From my understanding, null represents the intentional absence of a value, while undefined is a declared value that has not been provided. But I may be wrong on this one. What do you think? Should this function return null rather than undefined?

Anyway, good take on this one!

Collapse
starpebble profile image
starpebble

Here is an alternative with JSONPath Expressions, a tool for selecting data in a JSON structure by an expression kinda like XPath does for XML. Here is an expression example: "$.user.pets[0].toys[0].price" - to select the price for the first toy for the first pet for the user. A JavaScript object is a perfect fit for a JSONPath expression evaluation where the object contains nested arrays, dictionaries, and strings.

const {JSONPath} = require('jsonpath-plus');

const obj = {
  user: {
    name: "Joe",
    age: 20,
    pets: [
      {
        name: "Daffodil",
        type: "dog",
        toys: [
          {
            name: "Toughy",
            price: 1999
          }
        ]
      }
    ]
  }
};

console.log(JSONPath({path: '$.user.pets[0].toys[0].price', json: obj, wrap: false}));
// 1999

console.log(JSONPath({path: '$.user.dogs[0].toys[0].price', json: obj, wrap: false}));
// undefined

The npm package jsonpath-plus is a popular JSON Path evaluation engine though pick any one in the world you like! JSONPath probably wasn't invented by a computer. It's very user friendly.

lodash .get() is my second most favorite alternative. Kinda looks like some of use of lodash is going to get deleted when we all can use optional chaining. Which isn't a bad thing.

Collapse
damxipo profile image
Damian Cipolat

Nice! good article :D

Collapse
mburszley profile image
Maximilian Burszley

Oh I've got one:

import _ from 'lodash';

_.get(obj, 'obj.user.pets[0].toys[0].price');

Nifty, and doesn't re-invent the wheel.

Collapse
aminnairi profile image
Amin • Edited on

Shouldn't it be:

_.get(obj, "user.pets[0].toys[0].price");

Instead of:

_.get(obj, 'obj.user.pets[0].toys[0].price');

?

Plus, you don't want to import the whole 69kb of Lodash just for this matter. Since you are using ECMAScript Modules, you should use named imports instead.

import { get } from "lodash";

const obj = {
  user: {
    name: "Joe",
    age: 20,
    pets: [
      {
        name: "Daffodil",
        type: "dog",
        toys: [
          {
            name: "Toughy",
            price: 1999
          }
        ]
      }
    ]
  }
};

console.log(get(obj, "user.pets[0].toys[0].price"));
// 1999

Niftier, and does not import the unnecessary functions just to use the get helper.