Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Possibility to update a nested object's property in a non-mutable way #2948

Open
StudioSpindle opened this issue Feb 28, 2022 · 3 comments
Open
Labels
enhancement starter good choice for new contributors

Comments

@StudioSpindle
Copy link

Currently I am using something like:

const foo = bar.map((baz) => {
  return {
    ...baz,
    qux: baz.qux.map((quux) => {
       return {
         ...quux,
         aNewProperty: 'yay!'
       }
    }
  }
});

To make the point clear, it would be ideal to have something similar to the set method in lodash:

const foo = bar;
set(foo, 'bar.baz.qux.quux', { bar.baz.qux.quux, aNewPorperty: 'yay!' });

Note: I am not aware of how lodash and underscore are related but the current project I'm working on is using underscore. And at this time it's not possible to convert to lodash just for this single use-case.

@dogbubu
This comment was marked as off-topic.
@jgonggrijp
Copy link
Collaborator

jgonggrijp commented Feb 28, 2022

Yes, I agree it would make sense to have a set function in Underscore as well. In the meanwhile, you can define your own set function like this so it will be maintainable and reusable and integrate well with Underscore:

import _, { extend, isObject } from 'underscore';

var arrayIndex = /^\d+$/;

function keyValue(key, value) {
    var result = {};
    result[key] = value;
    return result;
}

function innerSet(obj, path, value) {
    if (!path.length) return value;
    var key = path[0];
    // Important: the next line prevents prototype pollution.
    if (key === '__proto__') throw new Error('Prototype assignment attempted');
    obj = obj || (arrayIndex.test(key) ? [] : {}); 
    value = innerSet(obj[key], path.slice(1), value);
    return extend(obj, keyValue(key, value));
}

function set(collection, path, value) {
    if (!isObject(collection)) return collection;
    path = _.toPath(path);
    return innerSet(collection, path, value);
}

Use it like this:

set(foo, ['bar', 'baz', 'qux', 'quux', 'aNewProperty'], 'yay!');

You can add it to Underscore so you can also use it in chaining:

import { mixin } from 'underscore';
import { set } from './your/own/module.js';

_.mixin({set});

_.chain({a: 1})
.extend({b: 2})
.set(['c', 0, 'd'], 3)
.value();
// {a: 1, b: 2, c: [{d: 3}]}

If you want to use shorthand dotted paths of the form 'bar.baz.qux.quux.aNewProperty', you can enable this by overriding _.toPath, as long as you keep in mind the warning that comes with that.

Note: I am not aware of how lodash and underscore are related but the current project I'm working on is using underscore. And at this time it's not possible to convert to lodash just for this single use-case.

Lodash is a fork of Underscore. Underscore is being actively maintained, so sticking with Underscore is fine.

@jgonggrijp jgonggrijp added enhancement starter good choice for new contributors labels Feb 28, 2022
@jgonggrijp
Copy link
Collaborator

I just noticed the "non-mutable" part of the issue title... sorry for missing that previously.

I want to avoid introducing new functions to Underscore that have the same name as a function in Lodash, but different semantics. So a set function in Underscore should be mutating. However, a setClone function is conceivable using _.clone, analogous to the code I wrote before:

import { clone } from 'underscore';

function innerSetClone(obj, path, value) {
    if (!path.length) return value;
    var key = path[0];
    // Important: the next line prevents prototype pollution.
    if (key === '__proto__') throw new Error('Prototype assignment attempted');
    obj = obj || (arrayIndex.test(key) ? [] : {}); 
    value = innerSetClone(obj[key], path.slice(1), value);
    return extend(clone(obj), keyValue(key, value));
}

function setClone(collection, path, value) {
    if (!isObject(collection)) return collection;
    path = _.toPath(path);
    return innerSetClone(collection, path, value);
}

It does exactly the same thing as set, except that it always returns a new object (or array) and never mutates existing objects.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement starter good choice for new contributors
Projects
None yet
Development

No branches or pull requests

3 participants