8

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

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

Seq

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

_(value).chained().method().value();
Steven Pribilinskiy
  • 1,862
  • 1
  • 19
  • 21

5 Answers5

4

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.

mik01aj
  • 11,928
  • 15
  • 76
  • 119
3

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 {}
Steven Pribilinskiy
  • 1,862
  • 1
  • 19
  • 21
1

Now you can use _.setWith

setWith({}, "some.deep.path", value, Object);

returning

{some:{deep:{path:4}}
Mow
  • 51
  • 2
1

You can go straight to the point in one line with

const object = _.set({}, 'some.deep.path', {value: 'foo'})

Or, if you already have an object that you want to update

const obj = {some: {deep: {path: {barbecue: ''}}}}
const path = 'some.deep.path'

_.set(obj, path, {..._.get(obj, path, {}), foo: 'bar'})
// => {some: {deep: {path: {barbecue: '', foo: 'bar' }}}}
gogaz
  • 2,323
  • 2
  • 23
  • 31
  • That would overwrite whole `some.deep.path` object instead of adding/changing the `value` (imagine that `some.deep.path` already contains `{ value: 'bar', otherValue: 'egg' }` – Steven Pribilinskiy Oct 26 '21 at 21:03
  • `_.set(obj, path, _.get(obj, path, {}));` is a good one even if it requires referencing `obj` and `path` twice. The task was to obtain a reference to an object at path and create that object at path if it doesn't exist. `_.set` returns the `obj` instead of `obj.some.deep.path.barbecue` ;) – Steven Pribilinskiy Oct 26 '21 at 21:56
0

How about this:

function getDefault(obj, path, defaultValue) {
  _.update(obj, path, d => d ? d : {})
  return _.get(obj, path);
}

If the path doesn't exist it will be created, otherwise it's not changed. Still not sure of a way to avoid the get though..

MattCochrane
  • 2,900
  • 2
  • 25
  • 35