1

I am trying to remove null & empty objects inside an object (but not falsy values), with recursively.

I have implemented logic without recursion not sure how to do with recursion, with deep nested objects.


const clearNullEmpties = (obj) => {
  let newObj = {}

  Object.entries(obj).forEach(([k, val], i) => {
    const isNotNull = val !== null
    const isObject = isNotNull && val.constructor === Object
    const isEmptyObj = isObject && Object.keys(val).length === 0

    (!isEmptyObj) && Object.assign(newObj, { [k]: val })

  })

  return newObj
}
// Example Data
let object = {
    a: {
        b: 1,
        c: {
            a: 1,
            d: {},
            e: {
              f: {} 
            }
        }
    },
    b: {}
}


let expectedResult = {
    a: {
        b: 1,
        c: {
            a: 1,
        }
    }
}

Note: I have gone throw this answer similar question but its not follows es6 (i don't want to modify(mutate) original object

dipenparmar12
  • 3,042
  • 1
  • 29
  • 39
  • Does this answer your question? [Recursively remove nullish values from a JavaScript object](https://stackoverflow.com/questions/59413342/recursively-remove-nullish-values-from-a-javascript-object) – customcommander Feb 07 '21 at 09:29
  • It does job well, but in my case I don't want to remove empty strings or empty arrays like. ' ', [] – dipenparmar12 Feb 07 '21 at 16:20
  • @customcommander By the way I will going to use only spread, because of your articles **Spread vs Assign (JS)** . – dipenparmar12 Feb 07 '21 at 16:30

3 Answers3

3

It's an interesting problem. I think it can be solved elegantly if we write a generic map and filter function that works on both Arrays and Objects -

function map (t, f)
{ switch (t?.constructor)
  { case Array:
      return t.map(f)
    case Object:
      return Object.fromEntries(Object.entries(t).map(([k, v]) =>  [k, f(v, k)]))
    default:
      return t
  }
}

function filter (t, f)
{ switch (t?.constructor)
  { case Array:
      return t.filter(f)
    case Object:
      return Object.fromEntries(Object.entries(t).filter(([k, v]) =>  f(v, k)))
    default:
      return t
  }
}

We can write your removeEmpties program easily now -

const empty =
  Symbol("empty") // <- sentinel

function removeEmpties (t)
{ switch (t?.constructor)
  { case Array:
    case Object:
      return filter(map(t, removeEmpties), nonEmpty)
    default:
      return nonEmpty(t) ? t : empty  // <-
  }
}

Now we have to define what it means to be nonEmpty -

function nonEmpty (t)
{ switch (t?.constructor)
  { case Array:
      return t.length > 0
    case Object:
      return Object.keys(t).length > 0
    default:
      return t !== empty // <- all other t are OK, except for sentinel
  }
}

Finally we can compute the result of your input -

const input =
    {a: {b: 1, c: {a: 1, d: {}, e: {f: {}}}}, b: {}}

console.log(removeEmpties(input))

Expand the snippet below to verify the result in your browser -

const empty =
  Symbol("empty")

function removeEmpties (t)
{ switch (t?.constructor)
  { case Array:
    case Object:
      return filter(map(t, removeEmpties), nonEmpty)
    default:
      return nonEmpty(t) ? t : empty
  }
}

function nonEmpty (t)
{ switch (t?.constructor)
  { case Array:
      return t.length > 0
    case Object:
      return Object.keys(t).length > 0
  //case String:             // <- define any empty types you want
  //  return t.length > 0
    default:
      return t !== empty // <- all other t are OK, except for sentinel
  }
}

function map (t, f)
{ switch (t?.constructor)
  { case Array:
      return t.map(f)
    case Object:
      return Object.fromEntries(Object.entries(t).map(([k, v]) =>  [k, f(v, k)]))
    default:
      return t
  }
}

function filter (t, f)
{ switch (t?.constructor)
  { case Array:
      return t.filter(f)
    case Object:
      return Object.fromEntries(Object.entries(t).filter(([k, v]) =>  f(v, k)))
    default:
      return t
  }
}

const input =
  {a: {b: 1, c: {a: 1, d: {}, e: {f: {}}}}, b: {}}

console.log(removeEmpties(input))
{
  "a": {
    "b": 1,
    "c": {
      "a": 1
    }
  }
}

If you don't want to include null or undefined -

function nonEmpty (t)
{ switch (t?.constructor)
  { case Array:
      // ...
    case Object:
      // ...
    default:
      return t != null && !== empty // <- 
  }
}

If you don't want to include empty strings, numbers, or false booleans -

function nonEmpty (t)
{ switch (t?.constructor)
  { case Array:
      // ...
    case Object:
      // ...
    case String:
      return t.length > 0  // <-
    case Number:
      return t != 0        // <-
    case Boolean:
      return !t            // <-
    default:
      // ...
  }
}

See this Q&A for a related approach and explanation of inductive reasoning.

Mulan
  • 129,518
  • 31
  • 228
  • 259
2

Something like this should do it.

const isNullOrEmpty = (obj) =>
  obj == null || (Object (obj) === obj && Object .keys (obj) .length == 0)

const clearNullEmpties = (obj) => 
  Object (obj) === obj
    ? Object .fromEntries (Object .entries (obj) .flatMap (([k, v]) => {
        const val = clearNullEmpties (v) 
        return isNullOrEmpty (val) ? [] : [[k, val]]
      }))
    : obj
 
const object = {a: {b: 1, c: {a: 1, d: {}, e: {f: {}}}}, b: {}}

console .log (clearNullEmpties (object))

We use Object.entries to break our object into key-value pairs, and then using flatMap, we combine a filtering and mapping operation on those pairs, recursively running the values through this function and keeping those ones that do not return an empty value, using our helper isNullOrEmpty. Then we recombine the results with Object.fromEntries.

If you need to handle arrays as well, that would take a little additional work, but shouldn't be hard.

Note that the above is a little more beginner-friendly than my personal preferred variant of this, which uses only expressions and no statements, and involves one more helper function:

const call = (fn, ...args)=>
  fn (...args)

const clearNullEmpties = (obj) => 
  Object (obj) === obj
    ? Object .fromEntries (Object .entries (obj) .flatMap (([k, v]) => call (
        (val = clearNullEmpties (v)) => !isNullOrEmpty (val) ? [[k, val]] : []
      )))
    : obj
Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • `call` has utility everywhere! i wonder if we will ever see a native syntax to support assignment expressions... – Mulan Feb 08 '21 at 17:05
  • @Thankyou and you both did well job and I learned lot in just one question. not sure which one should i mark as right Answer... confused. – dipenparmar12 Feb 08 '21 at 18:41
  • 1
    Choose whichever you like. I don't chase reputation points, and I doubt that Thankyou does either. I personally would choose the answer from Thankyou. It's more flexible and more generic. Mine has only the advantage of being shorter. – Scott Sauyet Feb 08 '21 at 18:51
  • 1
    A humble bunch, we are :D I am only here to participate with other passionated coders. I have learned so much from helping others and discussing these fun programming puzzles with my peers. – Mulan Feb 08 '21 at 20:01
1

Try like this:

const clearNullEmpties = (obj) => {
  let newObj = {}

  Object.entries(obj).forEach(([k, val], i) => {
    const isNotNull = val !== null
    const isObject = isNotNull && val.constructor === Object
    const isEmptyObj = isObject && Object.keys(val).length === 0

    /* call twice in case the result returned is iteslf an empty object,
    as with e in the Example Data */
    const result = (isObject && !isEmptyObj && clearNullEmpties(clearNullEmpties(val))) 
    
    if(isObject) {
      result && Object.assign(newObj, { [k]: result })
    } else if (isNotNull) {
      Object.assign(newObj, { [k]: (val) })
    }
  })

  return newObj
}


// Example Data
let object = {
    a: {
        b: 1,
        c: {
            a: 1,
            d: null,
            e: {
              f: {} 
            }
        }
    },
    b: {}
}


let expectedResult = {
    a: {
        b: 1,
        c: {
            a: 1,
        }
    }
}

console.log("result", clearNullEmpties(object))
console.log("expected", expectedResult)
sbgib
  • 5,580
  • 3
  • 19
  • 26