Alega Ahafonov
Alega Ahafonov

Reputation: 63

recursively remove undefined from object (including parent)

I'm trying to find a solution to a problem where I need to remove undefined from nested object including all parents if there are no values there, please consider example:

var test = {
  foo : {
    bar : {
      baz : undefined
    }
  },
  bar : 1
}

So my task is to remove baz along with bar and foo but still have bar at the root level; I know that it's trivial task to solve with 2 for loops, I'm just wondering if there are more elegant and clean solutions which will use recursive stack instead? Thanks in advance!

Upvotes: 5

Views: 4978

Answers (7)

T.J. Crowder
T.J. Crowder

Reputation: 1074276

Depth-first recursion should be able to handle it:

function cleanse(obj, path) {
    for (const key of allOwnKeys(obj)) {
        // Get this value and its type
        const value = obj[key];
        const type = typeof value;
        if (type === "object" && value !== null) {
            // Recurse...
            cleanse(value);
            // ...and remove if now "empty"
            if (objectIsEmpty(value)) {
                delete obj[key]
            }
        } else if (type === "undefined") {
            // Undefined, remove it
            delete obj[key];
        }
    }
}

function allOwnKeys(obj) {
    return [
        ...Object.getOwnPropertyNames(obj),
        ...Object.getOwnPropertySymbols(obj)
    ];
}

function objectIsEmpty(obj) {
    // NOTE: insert your definition of "empty" here, here's a starting point:
    if (    (obj instanceof Date) ||
            (obj instanceof Map && obj.size) ||
            (obj instanceof Set && obj.size) ||
            (obj instanceof WeakSet) || // You have to assume it's not empty
            (obj instanceof WeakMap)    // same
            // ... allow for String, Number, Boolean? ...
    ) {
        return false;
    }
    return (
        Object.getOwnPropertyNames(obj).length === 0 &&
        Object.getOwnPropertySymbols(obj).length === 0
    );
}

I've used Object.getOwnPropertyNames and Object.getOwnPropertySymbols to find all of the object's own properties because the more commonly-used Object.keys only checks for own, enumerable properties with string names, not inherited properties, non-enumerable ones, or Symbol-named ones.

Re "insert your definition of "empty" here":

  1. The special cases in objectIsEmpty are there because Date, Map, Set, WeakMap, and WeakSet objects don't have any own properties by default, but that doesn't necessarily mean they're empty. For Map and Set we can look at their size property to see if they have any elements in them; Date you probably always want to keep; for WeakMap, and WeakSet, you can't check if they have anything in them, so I'd assume you should keep them. Adjust objectIsEmpty to suit your needs, and note that you may want to update it as JavaScript continues to evolve.
  2. The code above doesn't try to handle Proxy objects in any special way.
  3. Although they're rarely used (and should never be used), there are object versions of strings, numbers, and booleans (value instanceof String, etc.); they also don't have any own, enumerable properties by default. I haven't bothered with them above because they shouldn't be used in the first place.

Example:

const test = {
    foo: {
        bar: {
            baz: undefined,
        },
    },
    bar: 1,
};
cleanse(test);
display(test);

const test2 = {
    a: {
        aa: {
            aaa: undefined,
        },
    },
    b: 1,
    c: {
        [Symbol()]: undefined,
    },
    d: {
        date: new Date(),
    },
    e: {
        emptyMap: new Map(),
    },
    f: {
        nonEmptyMap: new Map([["a", 1]]),
    },
};
cleanse(test2);
display(test2);

function cleanse(obj, path) {
    for (const key of allOwnKeys(obj)) {
        // Get this value and its type
        const value = obj[key];
        const type = typeof value;
        if (type === "object" && value !== null) {
            // Recurse...
            cleanse(value);
            // ...and remove if now "empty"
            if (objectIsEmpty(value)) {
                delete obj[key]
            }
        } else if (type === "undefined") {
            // Undefined, remove it
            delete obj[key];
        }
    }
}

function allOwnKeys(obj) {
    return [
        ...Object.getOwnPropertyNames(obj),
        ...Object.getOwnPropertySymbols(obj)
    ];
}

function objectIsEmpty(obj) {
    // NOTE: insert your definition of "empty" here, here's a starting point:
    if (    (obj instanceof Date) ||
            (obj instanceof Map && obj.size) ||
            (obj instanceof Set && obj.size) ||
            (obj instanceof WeakSet) || // You have to assume it's not empty
            (obj instanceof WeakMap)    // same
            // ... allow for String, Number, Boolean? ...
    ) {
        return false;
    }
    return (
        Object.getOwnPropertyNames(obj).length === 0 &&
        Object.getOwnPropertySymbols(obj).length === 0
    );
}

function display(obj) {
    console.log(JSON.stringify(
        obj,
        (key, value) => {
            if (value instanceof Map || value instanceof Set) {
                return [...value];
            }
            return value;
        },
        4
    ));
}
.as-console-wrapper {
    max-height: 100% !important;
}

Upvotes: 7

Salvador Aceves
Salvador Aceves

Reputation: 348

Without mutating the original object

const cleanse = obj => {
  const newObj = Array.isArray(obj) ? [...obj] : { ...obj };
  Object.keys(newObj).forEach((key) => {
    // Get this value and its type
    const value = newObj[key];
    var type = typeof value;
    if (type === "object") {
      // Recurse...
      newObj[key] = cleanse(value);
      // ...and remove if now "empty" (NOTE: insert your definition of "empty" here)
      if (!Object.keys(value).length) {
        delete newObj[key]
      }
    }
    else if (type === "undefined") {
      // Undefined, remove it
      delete newObj[key];
    }
  });
  return newObj;
};

console.log(
    cleanse({ a: { b: undefined, c: 22 }}),
    cleanse({ a: [{ b: undefined, c: 22 }] }),
);

Upvotes: 1

vishal
vishal

Reputation: 1

function cleanPayload(obj: any) {
  Object.keys(obj).forEach(key => {
    const value = obj[key];
    const type = typeof value;
    if (!value || !type) {
      delete obj[key];
    } else if (type === 'object') {
      cleanPayload(value);
      if (!Object.keys(value).length) {
        if (key != 'attributes') {
          delete obj[key];
        }
      }
    } 
  });
  return obj;
}

Upvotes: 0

ThaJay
ThaJay

Reputation: 1912

I took the function proposed by @T.J. Crowder and changed it to use for ... of and Object.entries(obj). I also return the object for convenience.

function cleanseObject(obj) {
    for (const [key, value] of Object.entries(obj)) {
        if (typeof value === 'object') {
            cleanseObject(value)
            if (!Object.keys(value).length) delete obj[key]
        } else if (typeof value === 'undefined') {
            delete obj[key]
        }
    }

    return obj
}

Upvotes: 1

Sean256
Sean256

Reputation: 3099

IMO this is much cleaner but probably a smidge slower

const cleanUndefined = object => JSON.parse(JSON.stringify(object));
const testWithoutUndefined = cleanUndefined(test)

Upvotes: 4

varsha ghodki
varsha ghodki

Reputation: 211

Here is the code, which will also remove that undefined contained key and empty object from the main object.

var test = {
  foo: {
    bar: {
      baz: undefined,
      bar: {
        baz: undefined,
      }
    }
  },
  bar: 1,
  baz: undefined
}

function loop(obj) {
  var t = obj;

  for (var v in t) {
      if (typeof t[v] == "object"){
         loop(t[v]);
         if(!Object.keys(t[v]).length){  
            delete t[v];
         }
      } else if (t[v] == undefined){
        delete t[v];
      }
   }
 
  return t;
}

var output = loop(test);
console.log(output);

Upvotes: -1

Pugazh
Pugazh

Reputation: 9561

Below example can help you get started.

Without delete keys with empty values:

var test = {
  foo: {
    bar: {
      baz: undefined,
      bar: {
        baz: undefined
      }
    }
  },
  bar: 1,
  baz: undefined
}

function loop(obj) {
  var t = obj;
  for (var v in t) {
    if (typeof t[v] == "object")
      loop(t[v]);
    else if (t[v] == undefined)
      delete t[v];
  }
  return t;
}

var output = loop(test);

console.log(output);

Deleting keys with empty values:

var test = {
  foo: {
    bar: {
      baz: undefined,
      bar: {
        baz: undefined
      }
    }
  },
  bar: 1,
  baz: undefined
}

function loop(obj) {
  var t = obj;
  for (var v in t) {
    if (typeof t[v] == "object")
      if (!t[v].length)
        delete t[v];
      else
        loop(t[v]);
    else if (t[v] == undefined)
      delete t[v];
  }
  return t;
}

var output = loop(test);

console.log(output);

Upvotes: 3

Related Questions