Home > Enterprise >  TypeScript: Push to an array of Nested JSON Object
TypeScript: Push to an array of Nested JSON Object

Time:04-14

I am trying to add and remove values from an array of NESTED json object. Deletion was easy but I am struggling with addition:

export const addNestedJson = (
  json: Record<string, any> | any[],
  value: any,
  keys: string[]
): Record<string, any> | any[] => {
  // Please pass in mutated Values before calling the function

  const isJSONArray = Array.isArray(json);
  if (keys.length === 0) {
    if (isJSONArray) {
      return [...json, value];
    }
    return json;
  }
  const key = keys.shift() as string;
  const index = key ?? 0;
  const indexKey = isNaN( index) ? 0 :  index;
  const newJson = isJSONArray ? json[indexKey] : json[key];
  const nestedJsonValue = addNestedJson(newJson, value, keys);
  return isJSONArray
    ? [...json, nestedJsonValue]
    : { ...json, [key]: nestedJsonValue };
}

I am trying to use recursion as that seems to be the easiest solution, but for some reason I am not able to visualize and formulate the merge logic.

Here's what I expect:

JSON: [], key: [], valueTobeAdded: 1 should return [1]

JSON: [{a: {b:[]}}], key: ['a','b'], valueTobeAdded: 1 should return [{a:{b:[1]}}]

JSON: [{a: {b:[{c:[4]}]}}], key: ['a','b','0','c'], valueTobeAdded: 1 should return [{a: {b:[{c:[4,1]}]}}]

CodePudding user response:

First of all, I think that the last two examples of expected output do not define a correct key. Since the inputs are arrays at their top level, and you expect the first array value to be replaced, you should have 0 as the first entry in the key.

In the code I see two issues and have a few more remarks:

  1. In case keys is the empty array, and the data is not an array, it should be replaced by the new value, so instead of return json, you should return value

  2. When coming back from the recursive call, and the current level is an array, the new value should not be appended to the array, but instead be the replacement value for what was at indexKey.

  3. Not a real problem, but I would not mutate keys.

  4. Not a problem with the algorithm, but the name json in your property and variable names is not appropriate. JSON is something you would have to pass to JSON.parse. Anything else should not be called JSON.

This results in the following adapted code:

const addNested = (
  data: Record<string, any> | any[],
  value: any,
  keys: string[]
): Record<string, any> | any[] => {
  const isArray = Array.isArray(data);
  if (keys.length === 0) {
    if (isArray) {
      return [...data, value];
    }
    return value; // Overwrite any data
  }
  // Don't mutate keys with shift
  const key = keys[0] as string;
  const indexKey =  key || 0;
  const newData = isArray ? data[indexKey] : data[key];
  const nestedValue = addNested(newData, value, keys.slice(1));
  return isArray
    ? Object.assign([], data, { [indexKey]: nestedValue }) // Replace at index
    : { ...data, [key]: nestedValue };
}

const addNested = (
  data /*: Record<string, any> | any[] */,
  value /*: any */,
  keys /*: string[] */
) /*: Record<string, any> | any[] */ => {
  const isArray = Array.isArray(data);
  if (keys.length === 0) {
    if (isArray) {
      return [...data, value];
    }
    return value; // Overwrite any data
  }
  // Don't mutate keys with shift
  const key = keys[0] /* as string */;
  const indexKey =  key || 0;
  const newData = isArray ? data[indexKey] : data[key];
  const nestedValue = addNested(newData, value, keys.slice(1));
  return isArray
    ? Object.assign([], data, { [indexKey]: nestedValue }) // Replace at index
    : { ...data, [key]: nestedValue };
}

console.log(addNested([], 1, [])); //  [1]
console.log(addNested([{a: {b:[]}}], 1, [0,'a','b'])); // [{a:{b:[1]}}]
console.log(addNested([{a: {b:[{c:[4]}]}}], 1, [0,'a','b','0','c'])); // [{a: {b:[{c:[4,1]}]}}]

CodePudding user response:

Couple of advices when working with algorithms:

  1. Recursion is actually quite hard for a human brain. Although I agree that some tasks are easier if you know how to apply recursion. Especially in case of nested recursion.
  2. Naming is really important.

So for example, for me, "keys" is quite confusing. If I revisit this function later, I would probably think at first that I should add the value to all of these keys. So I would rather rename the variable into smth like "keyPath".

Now, I'll try to explain how I would solve it. This is probably not the best solution, just something from the top of my head.

So, imagine we have a nested object:

{
    key1: {
        key2: {
            key3: []
        }
    }
}

And then, let's imagine the simplest scenario, we need to insert a value all the way to key3.

In this case, obviously keyPath=["key1", "key2", "key3"]

Now, in order to reach the last key, we can start with first key of the path, and try to go down into the object until we reach the end of keyPath.

  1. take first element of keyPath. const key = "key1";
  2. go deeper into our object: const nestedObj = obj[key];
  3. repeat, but now use nestedObj instead of obj, and "key2", and so forth

This can be a recursion, but I think it's a simple loop, isn't it?

for (const i=0; i<keyPath.length; i  ) {
   const key = keyPath[i];
   const newObj = obj[key];
   obj = newObj;
}

This code is a bit too verbose though. We can simplify it like this:

for (const key of keyPath) {
    obj = obj[key];
}

And voila, we got to the final object where we can insert the element:

obj.push(value);

So the whole function would look like this:

function addNested(obj: any, keyPath: (string|number)[], value: any) {
    for (const key of keyPath)
        obj = obj[key];
    obj.push(value);
}

This will already satisfy all of the test cases you had, but as I understood your code, there is one more possibility: last key in the keyPath doesn't exist or is a primitive value (i.e. not an object and not an array).

Well, when we loop into the object, if the key doesn't exist, our obj[key] will return undefined. And we can also check if it contains a primitive value or null.

So in this case, we know that we reached the end of the object, and we simply need to assign our value.

if (typeof obj[key] === 'undefined' || obj[key] === null || obj[key] !== Object(obj[key])) {
    obj[key] = value;
    return;
}

typeof obj[key] === 'undefined' || obj[key] === null can be simplified into obj[key] == null (notice it's == and not ===).

So the function becomes something like this:

function addNested(obj: any, keyPath: (string|number)[], value: any) {
    for (const key of keyPath) {
        if (obj[key] == null || obj[key] !== Object(obj[key])) {
            obj[key] = value;
            return;
        }
        obj = obj[key];
    }
    obj.push(value);
}

This is still not ideal algorithm, because it doesn't do any error checking. For example, if more than 1 key from the keyPath doesn't exist in the object, or keyPath ends with an object, it doesn't handle that.

Still, I hope that this example will help you to improve approach to working with algorithms :)

  • Related