jashkenas / underscore

JavaScript's utility _ belt
https://underscorejs.org
MIT License
27.29k stars 5.53k forks source link

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

Open StudioSpindle opened 2 years ago

StudioSpindle commented 2 years ago

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 commented 2 years ago

gsdg54sdg5s4d5gドっぱくラバニラサダウのブはとてもおいしそうなりんかマカョ♂

jgonggrijp commented 2 years ago

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 commented 2 years ago

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.