Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create an empty object at deep path with lodash

I need to create an empty object at a deep path but only if it is not exist and return a reference to that object.

This can be accomplished by using _.has, _.set and then _.get

let path = 'some.deep.path';

if (!_.has(object, path)) {
    _.set(object, path, {});
}

let descendant = _.get(object, path);

What I would like instead, is to avoid repeating the object, path (or introducing the path variable to avoid repeating the path value)

I wonder if there's a way to do that without extra function/library.


List of lodash methods that accept or deal with a path. Alphabetically

This includes methods that deal with arrays/collections and accept the _.property iteratee/predicate shorthand, i.e. when a string is provided as an argument, it is passed over to the _.property method that creates a function which returns the value at path of a given object (that was given by the main function, e.g. _.map).

Array

  • v4.0.0 _.differenceBy(array, [values], [iteratee=_.identity])
  • v3.0.0 _.dropRightWhile(array, [predicate=_.identity])
  • v3.0.0 _.dropWhile(array, [predicate=_.identity])
  • v1.1.0 _.findIndex(array, [predicate=_.identity], [fromIndex=0])
  • v2.0.0 _.findLastIndex(array, [predicate=_.identity], [fromIndex=array.length-1])
  • v4.0.0 _.intersectionBy([arrays], [iteratee=_.identity])
  • v4.0.0 _.sortedIndexBy(array, value, [iteratee=_.identity])
  • v4.0.0 _.sortedLastIndexBy(array, value, [iteratee=_.identity])
  • v3.0.0 _.takeRightWhile(array, [predicate=_.identity])
  • v3.0.0 _.takeWhile(array, [predicate=_.identity])
  • v4.0.0 _.unionBy([arrays], [iteratee=_.identity])
  • v4.0.0 _.uniqBy(array, [iteratee=_.identity])
  • v4.0.0 _.xorBy([arrays], [iteratee=_.identity])
  • v4.1.0 _.zipObjectDeep([props=[]], [values=[]])

Collection

  • v0.5.0 _.countBy(collection, [iteratee=_.identity])
  • v0.1.0 _.every(collection, [predicate=_.identity])
  • v0.1.0 _.filter(collection, [predicate=_.identity])
  • v0.1.0 _.find(collection, [predicate=_.identity])
  • v0.1.0 _.groupBy(collection, [iteratee=_.identity])
  • v4.0.0 _.invokeMap(collection, path, [args])
  • v0.1.0 _.map(collection, [iteratee=_.identity])
  • v3.0.0 _.partition(collection, [predicate=_.identity])
  • v0.1.0 _.reject(collection, [iteratee=_.identity])
  • v0.1.0 _.some(collection, [iteratee=_.identity])

Math

  • v4.0.0 _.maxBy(array, [iteratee=_.identity])
  • v4.7.0 _.meanBy(array, [iteratee=_.identity])
  • v4.0.0 _.minBy(array, [iteratee=_.identity])
  • v4.0.0 _.sumBy(array, [iteratee=_.identity])

Object

  • v1.0.0 _.at(object, [paths])
  • v1.1.0 _.findKey(object, [predicate=_.identity])
  • v2.0.0 _.findLastKey(object, [predicate=_.identity])
  • v3.7.0 _.get(object, path, [defaultValue])
  • v0.1.0 _.has(object, path)
  • v4.0.0 _.hasIn(object, path)
  • v4.0.0 _.invoke(object, path, [args])
  • v2.4.0 _.mapValues(object, [iteratee=_.identity])
  • v0.1.0 _.omit(object, [paths])
  • v0.1.0 _.pick(object, [paths])
  • v0.1.0 _.result(object, path, [defaultValue])
  • v3.7.0 _.set(object, path, value)
  • v4.0.0 _.setWith(object, path, value, [customizer])
  • v4.0.0 _.unset(object, path)
  • v4.6.0 _.update(object, path, updater)
  • v4.6.0 _.updateWith(object, path, updater, [customizer])

Util

  • v4.0.0 _.iteratee([func=_.identity])
  • v3.2.0 _.matchesProperty(path, srcValue)
  • v3.7.0 _.method(path, [args])
  • v3.7.0 _.methodOf(object, [args])
  • v2.4.0 _.property(path)
  • v3.0.0 _.propertyOf(object)
  • v4.0.0 _.toPath(value)

Seq

Chained methods, i.e. methods that are called in a sequence, e.g.:

_(value).chained().method().value();
  • v1.0.0 _.prototype.at([paths])
like image 480
Steven Pribilinskiy Avatar asked Sep 04 '15 11:09

Steven Pribilinskiy


2 Answers

You can avoid the _.has with this:

var path = 'some.deep.path',
descendant = _.get(object, path);
if (!descendant) {
    descendant = {};
    _.set(object, path, descendant);
}

Thus traversing the path only 2 times, instead of 3.

like image 139
mik01aj Avatar answered Oct 02 '22 01:10

mik01aj


There's a _.deepDefault method in an extra lodash-deep library that checks if the value at the propertyPath resolves to undefined, and sets it to defaultValue if this is the case:

var descendant = _.deepDefault(object, 'some.deep.path', {});

That library is not updated anymore because Lodash now supports most of the functionality natively so here's an implementation as a lodash mixin function:

function getOrSetDefault(object, path, defaultValue) {
    const descendant = _.get(object, path);
    if (descendant === undefined) {
        _.set(object, path, defaultValue);
    }
    return descendant;
}

_.mixin({ getOrSetDefault });

const a = _.getOrSetDefault(object, 'some.deep.path', 42);
const b = _.getOrSetDefault(object, 'some.other.deep.path', {});
b.c = 42;

With the new optional chaining operator and a logical nullish assignment there would be no need to use lodash for this particular case (if it is acceptable to populate/define some.deep.path with a default value), e.g.:

some?.deep?.path ??= {}
const { foo } = some.deep.path

Unfortunately optional chaining assignments are not available yet. See https://stackoverflow.com/a/58882121/1949503

There would be just one drawback in that there'll be a need to repeat property accessors (some.deep.path) to retrieve the value. However in this case we will have autocompletion using Typescript/JSDoc unlike in lodash funcions' path argument (either string or array)

If there's only a need to get value at path the vanilla try/catch could be sufficient:

let foo: Foo;
try {
  foo = some.deep.path.foo
} catch {}
like image 40
Steven Pribilinskiy Avatar answered Oct 02 '22 01:10

Steven Pribilinskiy