The answer from Nick Parsons is great, both the well-written code and the excellent explanation.
But I think there is a possibility to write this both more generically and more simply.
While the requirement here is to search by id, we can easily imagine all sorts of other ways we want to match. Rather than trying to anticipate them all, we can write a generic version that takes an arbitrary predicate, and then configure it with a simple function.
Here is one approach:
const deepFind = (pred) => ([x, ...xs] = []) =>
x && (pred (x) ? x : deepFind (pred) (x.child) || deepFind (pred) (xs))
const findById = (id) => (obj) =>
deepFind ((o) => o.id == id) ([obj])
const input = {id: "A", name: "Item A", child: [{id: "B", name: "Item B", child: [{id: "C", name: "Item C", child: []}]}, {id: "D", name: "Item D", child: []}]};
console .log ('C:', findById ('C') (input)) //~> {id: "C", name: "Item C", child: []}
console .log ('X:', findById ('X') (input)) //~> undefined
deepFind accepts a predicate function and returns a function that accepts an array and returns the first match when searching depth-first (on a node -> node.child tree) for a match to the predicate.
We wrap this with findById, which takes a target id, and returns a function which takes an input object, wraps that object in an array, and calls deepFind with a predicate to test that the id matches the target and with that array.
We could easily make this more generic by adding another initial parameter to deepFind to tell us how our tree is structured. (node -> node.children is probably more common.) But that's a task for another day.
deepFind has perhaps a slightly dense implementation. It can be written in various ways, if one of these makes more sense to you:
const deepFind = (pred) => (xs) => {
for (let x of xs) {
if (pred (x)) {return x}
return deepFind (pred) (x .child || [])
}
}
or
const deepFind = (pred) => ([x, ...xs]) =>
x == undefined
? undefined
: pred (x)
? x
: deepFind (pred) (x.child || []) || deepFind (pred) (xs)
or atop a traverse generator function:
function * traverse (xs = []) {
for (let x of xs) {
yield x;
yield * traverse (x.child || [])
}
}
const deepFind = (pred) => (obj) => {
for (let node of traverse ([obj])) {
if (pred (node)) {return node}
}
}
and we could find many more.