7

I have the following code to get a key for a given object. So for the object {"hello": {"data": {"a": "world", "b": "random"}}} if the key passed in was hello.data.a, the output would be world.

(object, key) => {
    const keyParts = key.split(".");
    let returnValue = object;
    keyParts.forEach((part) => {
        if (returnValue) {
            returnValue = returnValue[part];
        }
    });
    return returnValue;
}

I'm trying to figure out how I can delete a property dynamically in the same fashion. So for example for the same object and key, it would mutate the object to be {"hello": {"data": {"b": "random"}}}.

Basically I want the same behavior as delete object.hello.data.a. But the problem is the hello.data.a part is a dynamic string passed in. Of course something like delete object[key] wouldn't work since the key has nested levels. It would work if it is only 1 level deep, but wouldn't work for nested items.

One other important note is that performance is extremely important in this case. So although creating a new object and copying over all the properties except for the one I want to delete might work, but I have severe concerns of the performance impact of that vs the delete keyword.

Due to other external factors I also do not want to set the property value to null or undefined. I want the key to actually be removed from the object.

How can I achieve this?

Charlie Fish
  • 18,491
  • 19
  • 86
  • 179

4 Answers4

6

To delete, .pop() off the last key to identify the property to remove. If you want to avoid the delete keyword, you'll also want to replace the last object on which that key is on, rather than mutate it, so .pop() off the next last key as well.

Then, use your current method (or .reduce) to access the last outer object. To delete one of its keys:

  • To mutate with delete, using the saved key, use delete[nextLastKey][lastKey]
  • To create an entirely new object but without the saved key, use rest syntax to exclude that lastKey during the creation of a new one, then assign the new object to the nextLastKey of the parent object:

const fn = (object, key) => {
    const keys = key.split(".");
    const lastKey = keys.pop();
    const nextLastKey = keys.pop();
    const nextLastObj = keys.reduce((a, key) => a[key], object);
    
    // delete version:
    // delete nextLastObj[nextLastKey][lastKey]
    
    // non-delete version:
    const { [lastKey]: _, ...rest } = nextLastObj[nextLastKey];
    nextLastObj[nextLastKey] = rest;
    return object;
}

const obj = {"hello": {"data": {"a": "world", "b": "random"}}};
const result = fn(obj, 'hello.data.a');
console.log(result);
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • This will work for as many nested objects or as deep as the key is, correct? – Charlie Fish Mar 27 '20 at 23:21
  • Yes, the key can be arbitrarily deep. Though, it can't be arbitrarily *shallow* - eg `{ foo: 'foo' }` cannot have the `foo` property removed from it without using `delete` – CertainPerformance Mar 27 '20 at 23:23
  • Got it! For that I could probably just code in a conditional to check to see if a `.` exists in the `key` variable and just delete that normally by doing `delete object[key]`. – Charlie Fish Mar 27 '20 at 23:25
  • This solution so far looks great and passes the tests I wrote. Will need a bit more customization to get it to work with my use case but I think it'll work perfectly. If possible, would love a bit more explanation for why this works. It looks like it's due to `nextLastObj` being a reference as opposed to a copy of the value, and deleting that would work? Would love it if you could maybe elaborate a bit further on your answer so myself and others can understand more about why this works, and the underlying concepts at play here. Thanks so much for your help tho!! – Charlie Fish Mar 27 '20 at 23:30
  • You need to replace the last object to "delete" a key from it without using `delete`. With *only* the last key, you can't replace the last object (for the same reason the `{ foo: 'foo' }` plus `'foo'` won't work), but with the next to last key, you can replace the last object on its parent. By saving the next to last key, you can reassign on the parent object. – CertainPerformance Mar 27 '20 at 23:39
1

Actually your answer is right there - in the code snippet that you provided. It just needs a little modification, like so:

const delProp = (object, key) => {
    const keyParts = key.split(".");
    let returnValue = object;
    
    let parent, lastKey;  // added
    keyParts.forEach((part) => {
        if (returnValue) {
            parent = returnValue; // added
            lastKey = part;  // added
            returnValue = parent[lastKey];
        }
    });
    
    if(parent) {
      delete parent[lastKey];  // added
    }
    
    return returnValue;
}


const obj = {"hello": {"data": {"a": "world", "b": "random"}}};

console.log(delProp(obj, 'hello.data.a'));
console.log(obj)

Also note that in JS for loops are essentially more efficient than forEach Array function. Thus I would change the related part a la:

for (const i = 0, i = keyParts.length; i++) {
    // ...
}
Kenan Güler
  • 1,868
  • 5
  • 16
0

you can use lodash library _.omit function

_omit(object, path)

https://lodash.com/docs/4.17.15#omit

be careful cause this function doesn't mutate the object instead it returns a new object without the removed key.

-2

You can still delete using the bracket syntax.

I added another example since the naysayers seem to think it cannot be done. As long as you know the basic object it should make no difference. I will leave the split by "." as a simple exercise here as it is not part of the core question.

var thing = {"hello": {"data": {"a": "world", "b": "random"}}};
delete thing.hello.data["a"];
console.log(thing);


var thing2 = {"hello": {"data": {"a": "world", "b": "random"}}};
delete thing2["hello"]["data"]["a"];
console.log(thing2);
Mark Schultheiss
  • 32,614
  • 12
  • 69
  • 100
  • 1
    Remember, the input string is `hello.data.a`, which is dynamic and can change at anytime. – Charlie Fish Mar 27 '20 at 23:21
  • 2
    I downvoted this answer, because as the question author said, The path to the property is dynamic, so the standard way `delete thing.hello.data["a"];` is not a solution – Dahou Mar 27 '20 at 23:22
  • 1
    @MarkSchultheiss I don't see how your answer can help here, Even the question include the same code you've posted and saying that it won't work, You wrote that the spliting part is not the core of the question, but it is. Basically you left the whole answer as an exercise! – Dahou Mar 29 '20 at 23:52