#Objects#Algorithms

Recursive Object Evaluation

Implement a function that recursively traverses an object, evaluates functions with given arguments, and transforms keys to lowercase. Master recursive object traversal and type checking.

By Pratik RaiMedium

Walking a nested object — to flatten its keys, resolve its values, redact its secrets or diff it against another — is a task that appears constantly in real code and almost as often in interviews. The traversal is straightforward. What makes it interesting is the list of things that are technically objects and will break a naive implementation.

The naive version, and everything wrong with it

JSfile.javascript
1function walk(obj) { 2 for (const key in obj) { 3 if (typeof obj[key] === 'object') { 4 walk(obj[key]); 5 } else { 6 console.log(key, obj[key]); 7 } 8 } 9}

Four distinct bugs live in those six lines.

typeof null === 'object'. The oldest wart in the language. A null value recurses, and for...in over null does nothing, so the value is silently dropped rather than reported.

Arrays are objects. typeof [] === 'object', so arrays get walked as though their indices were meaningful keys. Sometimes that is what you want; it should be a decision, not an accident.

for...in walks the prototype chain. It enumerates inherited enumerable properties too. On a plain object literal that is harmless, but on a class instance or anything with a modified prototype you get keys you never set. Object.keys or Object.entries covers own properties only.

Circular references hang forever. obj.self = obj produces infinite recursion and a stack overflow.

A version that holds up

JSfile.javascript
1function isPlainObject(value) { 2 return ( 3 typeof value === 'object' && 4 value !== null && 5 !Array.isArray(value) && 6 !(value instanceof Date) && 7 !(value instanceof RegExp) && 8 !(value instanceof Map) && 9 !(value instanceof Set) 10 ); 11} 12 13function flattenObject(input, { delimiter = '.' } = {}) { 14 const result = {}; 15 const seen = new WeakSet(); 16 17 function walk(value, path) { 18 if (!isPlainObject(value)) { 19 result[path] = value; 20 return; 21 } 22 if (seen.has(value)) { 23 result[path] = '[Circular]'; 24 return; 25 } 26 seen.add(value); 27 28 const entries = Object.entries(value); 29 if (entries.length === 0) { 30 result[path] = {}; // preserve empty objects rather than losing them 31 return; 32 } 33 34 for (const [key, child] of entries) { 35 walk(child, path ? `${path}${delimiter}${key}` : key); 36 } 37 } 38 39 walk(input, ''); 40 return result; 41}
JSfile.javascript
1flattenObject({ user: { name: 'Ada', meta: { active: true } }, id: 1 }); 2// { 'user.name': 'Ada', 'user.meta.active': true, id: 1 }

The WeakSet is the right structure for cycle detection: it holds object references without preventing garbage collection, and membership testing is O(1). A plain array with includes would be O(n) per check and would keep every visited object alive.

The empty-object branch matters more than it looks. Without it, {a: {}} produces {} — the key disappears entirely, because there are no leaves beneath it to record.

The things that are technically objects

This is the list worth memorising, because each one silently misbehaves under a naive typeof check:

ValuetypeofNaive traversal does
null'object'recurses into nothing, drops the value
[]'object'walks indices as keys
new Date()'object'flattens to {} — no own enumerable keys
new Map()'object'flattens to {} — entries are internal
/regex/'object'flattens to {}
() => {}'function'usually skipped, sometimes wanted

Date, Map, Set and RegExp are the quiet ones. They have no own enumerable properties, so they do not throw — they just vanish into empty objects, and you find out when a timestamp turns into {} in production.

Choosing how to treat arrays

There is no universally correct answer, only a decision to make explicitly:

JSfile.javascript
1// Keep arrays intact 2{ tags: ['a', 'b'] }{ tags: ['a', 'b'] } 3 4// Index into them 5{ tags: ['a', 'b'] }{ 'tags.0': 'a', 'tags.1': 'b' } 6 7// Bracket notation, as query-string libraries do 8{ tags: ['a', 'b'] }{ 'tags[0]': 'a', 'tags[1]': 'b' }

Form libraries index. Config loaders usually keep arrays whole. Pick one, document it, and make it an option.

Where the pattern shows up

Flattening config so environment variables can override nested keys (DATABASE__HOSTdatabase.host).

Redacting secrets before logging — the same traversal, replacing values whose key matches /password|token|secret/i.

Deep equality and diffing — walk both objects in step and compare leaves.

Form statereact-hook-form and similar use dotted paths as field names, which is exactly this transformation.

Prototype pollution

If you are building the inverse — turning 'a.b.c' back into a nested object — treat the key path as untrusted. A path of __proto__.isAdmin in a naive unflatten writes to Object.prototype and affects every object in the program. This is a real, exploited vulnerability class:

JSfile.javascript
1const FORBIDDEN = new Set(['__proto__', 'constructor', 'prototype']); 2if (FORBIDDEN.has(key)) continue;

Building the result with Object.create(null) removes the risk entirely, since there is no prototype to pollute.

Key takeaways

  • typeof null === 'object' — check for null before recursing, always.
  • Use Object.entries, not for...in, to avoid inherited keys.
  • Date, Map, Set and RegExp flatten to {} unless you exclude them explicitly.
  • Track visited objects in a WeakSet to survive circular references.
  • Handle empty objects, or the key disappears.
  • When reversing the transformation, reject __proto__ and constructor keys.

Goal: Implement the recursive object evaluation function within 30-45 minutes. Focus on understanding recursive traversal and type checking.

Frequently asked questions

What is this problem actually testing?
Traversing a structure of unknown shape and depth without losing your place. Objects contain objects, arrays contain objects, values may be functions to invoke — and the transformation has to apply everywhere while keeping the original structure intact.
Why must you check for arrays separately?
Because `typeof [] === "object"`. Treating an array as a plain object and rebuilding it with `Object.entries` turns it into an object with numeric string keys, and the shape silently changes. `Array.isArray` first, then the object branch.
What else lies about being an object?
`typeof null === "object"`, a language bug old enough that fixing it would break the web. A recursion that does not guard against `null` will try to read its properties and throw. `null` is the input that breaks most first attempts.
What are the follow-ups?
Circular references, where a naive recursion never terminates — the fix is a `WeakMap` of already-visited objects. And whether the transformation should mutate or return a new structure; a new one is almost always right, for the same reason immutable state updates are.

Related Challenges

Continue learning with these related challenges

View All
JavaScript

Object.assign Polyfill

Implement Object.assign from scratch: copy own enumerable properties (including Symbols) from sources onto the target.

JavaScript · Objects · PolyfillsPratik Rai ·

JavaScript

Object.create Polyfill

Implement Object.create using the classic empty-constructor trick to set an object's prototype chain.

JavaScript · Objects · PrototypesPratik Rai ·

JavaScript

Deep Equality

Compare two values structurally, because === only ever compares references.

JavaScript · ES6Pratik Rai ·