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

3
  • Does this answer your question? Recursively remove nullish values from a JavaScript object Commented Feb 7, 2021 at 9:29
  • It does job well, but in my case I don't want to remove empty strings or empty arrays like. ' ', [] Commented Feb 7, 2021 at 16:20
  • @customcommander By the way I will going to use only spread, because of your articles Spread vs Assign (JS) . Commented Feb 7, 2021 at 16:30

3 Answers 3

4

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.

Sign up to request clarification or add additional context in comments.

Comments

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

4 Comments

call has utility everywhere! i wonder if we will ever see a native syntax to support assignment expressions...
@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.
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.
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.
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)

3 Comments

If you change, say, d to null, it gets included in the output. I think that's unwanted.
Then you might want to see my answer, or even better, the one from the always-brilliant Thankyou.
Thanks for pointing that out! I've fixed it now.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.