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.