Chalarangelo/30-seconds-of-code

View on GitHub
content/snippets/js/s/get-nested-object-value.md

Summary

Maintainability
Test Coverage
---
title: Get a nested object property by key or path in JavaScript
shortTitle: Get nested object property
type: story
language: javascript
tags: [object,regexp,recursion]
cover: campfire
excerpt: Learn how to get a nested object property by key or a path string in JavaScript, and how to search for nested values in an object.
listed: true
dateModified: 2024-03-22
---

Working with objects, you'll often need to **retrieve nested properties**. It's not uncommon to have deeply nested objects and keys that are **calculated dynamically** and not known in advance. This means that you'll have to dynamically find the value of a nested property based on a key or a path string, or search for a property in an object.

## Use an array of keys to get a nested object property

The simplest scenario and by far the most common is having an **array of keys** that represent the path to the desired property in the object. In that case, all you need to do is use `Array.prototype.reduce()` to **iterate over the keys** and get the nested property. If the key doesn't exist, you can return `null`.

In order to keep the syntax concise, you can use the nullish coalescing operator (`??`) and the optional chaining operator (`?.`) to handle cases where **the property doesn't exist**.

```js
const deepGet = (obj, keys) => keys.reduce((xs, x) => xs?.[x] ?? null, obj);

const data = {
  foo: {
    foz: [1, 2, 3],
    bar: { baz: ['a', 'b', 'c'] },
  },
};

deepGet(data, ['foo', 'foz', 2]); // 3
deepGet(data, ['foo', 'bar', 'baz', 8, 'foz']); // null
```

## Use path strings to get nested object properties

A less common, yet more complex use-case is when you need to get a nested object property based on a **path string**. This is useful when you have a string that represents the path to the desired property, like `'foo.bar.baz'`.

In that case, you will have to normalize the path string and **split it** into an array of keys. The **normalization** process involves using `String.prototype.replace()` to replace square brackets with dots, and then splitting the string via `String.prototype.split()`. As this might produce empty strings, you should filter them out, using `Array.prototype.filter()`.

The resulting value is an array of keys that you can pass to the previous function. Additionally, you can use **rest parameters** in order to allow for **multiple path strings** to be passed to the function at once.

```js
const deepGet = (obj, keys) => keys.reduce((xs, x) => xs?.[x] ?? null, obj);

const deepGetByPaths = (obj, ...paths) =>
  paths.map(path =>
    deepGet(
      obj,
      path
        .replace(/\[([^\[\]]*)\]/g, '.$1.')
        .split('.')
        .filter(t => t !== '')
    )
  );

const data = {
  foo: {
    foz: [1, 2, 3],
    bar: { baz: ['a', 'b', 'c'] },
  },
};
deepGetByPaths(data, 'foo.foz[2]', 'foo.bar.baz.1', 'foo[8]');
// [3, 'b', null]
```

## Search for a deeply nested property in an object

Another unusual scenario is searching for a deeply nested property in an object. This is useful when you **don't know the exact path** to the property, but you know the key you're looking for. In this case, you can use a **recursive function** that will search for the key in the object and its nested properties.

For this scenario, you can use the `in` operator to check if the **target key exists** in the object. If it does, you can return the value of the key. If it doesn't, you can use `Object.values()` and `Array.prototype.reduce()` to recursively call the function on each nested object until the **first matching key/value pair** is found.

```js
const dig = (obj, target) =>
  target in obj
    ? obj[target]
    : Object.values(obj).reduce((acc, val) => {
        if (acc !== undefined) return acc;
        if (typeof val === 'object') return dig(val, target);
      }, undefined);

const data = {
  foo: {
    foz: [1, 2, 3],
    bar: { baz: ['a', 'b', 'c'] },
  },
};

dig(data, 'foz'); // [1, 2, 3]
dig(data, 'baz'); // ['a', 'b', 'c']
```

> [!NOTE]
>
> The behavior of `dig` when the target key is an integer is to return the value at the given array index. This, in turn, means that the **first array to be encountered** will be the one that contains the target value.