It makes sense to me to separate out the recursive traversal and filtering from the actual details of removing a certain id. So I would write a more generic filterDeep function which keeps only those objects where a predicate is true, recursing into children nodes.
Then we can supply a predicate to it that checks whether an item matches a specific id. Or rather, since we're removing those that match, our predicate actually checks whether our node doesn't match the id.
This is an implementation of that idea:
const filterDeep = (pred) => (xs) =>
xs .flatMap (x => pred (x)
? [{... x, children: filterDeep (pred) (x .children || [])}]
: []
)
const removeId = (id) =>
filterDeep (x => x.id !== id)
const myArray = [{id: 1, children: [{id: 3, children: []}]}, {id: 2, children: []}]
console .log (removeId (1) (myArray))
console .log (removeId (2) (myArray))
console .log (removeId (3) (myArray))
console .log (removeId (42) (myArray))
.as-console-wrapper {min-height: 100% !important; top: 0}
This will include a children node even if the original didn't have one. If we wanted to include it only if there was one to begin with we could change it to something like this:
const filterDeep = (pred) => (xs) =>
xs .flatMap (x => pred (x)
? [{
... x,
... (x.children ? {children: filterDeep (pred) (x .children || [])} : {})
}]
: []
)
Or with a little more sophistication, we could choose to include a children node only when it's available in the parent and the results are non-empty. That's left as an exercise for the reader. :-)