This question has already attracted a nice variety of high-quality answers, and others have explained why your attempt fails.
I'd like to offer a variant of what user Thankyou suggested. We can flatten the nested structure into an array of objects, using a preorder traversal. Then if you need to do something involving all the the nodes, you can work with an array, which is often simpler. The code for that is straightforward:
const flatten = (prop) => (o) =>
[o, ... (o [prop] ?? []) .flatMap (flatten (prop))]
const sum = (ns) => ns.reduce ((a, b) => a + b, 0)
const totalAges = (xs) =>
sum (flatten ('kids') (xs) .map (p => p.age))
const profile = {name: 'peter', age: 56, kids: [{name: 'jill', age: 23, kids: [{name: 'jeter', age: 1}, {name: 'bill', age: 2}]}]}
console .log (totalAges (profile))
Here flatten
takes a string representing the property name of items' children and returns a function that takes an object and traverses it preorder, collecting all the nodes in an array. We call this in totalAges
, then pluck off the age
properties of each node, finally passing this array to a simple sum
function.
This is a simple enough technique, but we might want to make a generic version that passes a function such as node => node.age
to directly collect the ages from our traversal. This turns out to be nearly as simple:
const gather = (prop) => (fn) => (o) =>
[fn (o), ... (o [prop] ?? []) .flatMap (gather (prop) (fn))]
const sum = (ns) => ns.reduce ((a, b) => a + b, 0)
const totalAges = (xs) =>
sum (gather ('kids') (p => p.age) (xs))
const profile = {name: 'peter', age: 56, kids: [{name: 'jill', age: 23, kids: [{name: 'jeter', age: 1}, {name: 'bill', age: 2}]}]}
console .log (totalAges (profile))
Here totalAges
is slightly simpler as we hand off the application of p => p.age
to our gather
function. But the general logic is much the same.
This technique has one advantage over Thankyou's generator based-technique, and has two disadvantages that I know of. The first disadvantage is that this is subject to recursion depth-limits. Even if we fiddle to make this tail-call optimizable, most engines are still not taking advantage of that, so it could fail on extremely deeply nested object. (This would probably require an object with nodes thousands of levels deep; and if you have such a structure, you probably have other pressing problems too. But it's a valid concern.)
The second disadvantage is that this traversal cannot be cancelled. With a generator-based approach, you can stop early if you decide you're done. Perhaps you want to total the ages only so long as the total does not exceed 1000. That would be trivial in the generator-based approach, but is not really possible here. You could stop summing, but this technique would still traverse the entire tree.
The advantage is one of simplicity. The code to use this is often simpler, partly because there are many more utilities available for working with arrays than for working with iterators. You also get to work with pure expressions, and don't have to fiddle with loop variables and such.
Mind you, this advantage is minor. It's often enough for me to choose this technique over the generator one, but either should work well in practice.
(One minor note: there is no fundamental difference between these two approaches regarding the abstraction of prop
and the value of kids
. This version could easily be modified to embed kids
in the main function, and in fact a partial application will create that, leaving only object -> array
. And Thankyou's technique could quite easily be abstracted the way this one is with a prop
parameter. That is much more a matter of taste than a fundamental difference.)