Lodash get() does not default for value null

Lodash get() does not default for value null

ยท

3 min read

It's been 1+ years of using lodash. I have no complaints except for it being too large on the bundle.

However, recently I have found an issue that I thought was handled by lodash.get(), but it is not. ๐Ÿซ 

The Story

_.get() is the most used utility function in my code. Recently the following lines of code broke in quite a few places.

const todos = get(apiData, ["data", "todos"], []);
const updatedTodos = todos.map((todo) => ({ ...todo, key: todo.id }));

Debug

So why is the above code breaking? ๐Ÿ’”

To understand this let's read the code.

  1. On Line-1 we are saying get apiData.data.todos if not assign [] to todos.

  2. On Line-2 we are simply looping over the array from the previous line to add a property key.

Everything sounds right?

But here is the catch! ๐Ÿ’€

WE ARE TRUSTING LINE 1 TO RETURN AN ARRAY!

.map() can only be used on Array. So the code breaks on Line-2 if todos is not an array.

Essentially my code broke for todos being null in some cases.

How can this be possible? We are defaulting todos to [] if we don't find apiData.data.todos. Seems handled right?

No! we are mistaking null for undefined.

Didn't find the expected key technically === undefined in Javascript.

So lodash get() takes the default value only if it encounters undefined in the given path (here in this example: apiData.data.todos ) but not for any value else ( null )

Lodash mentions the same in its docs.

It is now clear that lodash does not take the default value if the end value is null.

Now! How to fix this? ๐Ÿค”

The Fix

The fix is simple.

  1. We can use optional chaining todos?.map on Line-2.

     const updatedTodos = todos?.map((todo) => ({ ...todo, key: todo.id }));
    

    OR

  2. We can assign [] to todos, if the value is null on Line-1.

     const todos = get(apiData, ["data", "todos"], []) ?? [];
    

There is a downside to Fix-1.

updatedTodos will be undefined if todos is nullish ( undefined or null ) which is not intended at all times. Consider a situation where you are looping updatedTodos somewhere down in the code. Then the same issue arises.

Always meet the type expectations. If your Line-2 expects an array, Line-1 should take care of it in any case.

Using typescript would help in these cases. But lodash.get() cannot infer value type so you need to handle it explicitly.

So we are going ahead with Fix-2. But do you see the repeated []?

Let's refactor โ™ป

๐Ÿ“Œ The Nullish coalescing operator (??) returns the right-hand side operand when its left-hand side operand is null or undefined.

const todos = get(apiData, ["data", "todos"]) ?? [];

The Solution

The above fix works perfectly fine. โœ…

But it is not scalable. ๐Ÿ“

In the above code, we need to add ?? [] everywhere we need a default value and the same applies to similar use cases like using a default value for an Empty string "" and we also need to repeat this where ever required.

If something is repeated in code, then it can be refactored.

Let's create a utility function that takes care of this.

Design: get(object, path, defaultValue)

Functionality: returns defaultValue if the value turns out nullish else returns the value.

// utility.ts
import lodashGet from "lodash/get";

type TGetParams = Parameters<typeof lodashGet>;

export function get(
  object: TGetParams[0],
  path: TGetParams[1],
  defaultValue?: TGetParams[2]
) {
  const value = lodashGet(object, path);

  if (defaultValue !== undefined) {
    return value ?? defaultValue;
  }

  return value;
}

We can now use this new get() function in your code if you plan to use the default value for nullish values. ๐Ÿš€

You can rename this function to getNonNullish() or lodash get function to lodashGet() to avoid using them interchangeably.


PS: Suggest an alternate name for getNonNullish based on its functionality in the comments. ๐Ÿ™‚

ย