Traverse a deeply nested object and resolve values of all functions inside it
Recursively walk the structure; if a value is a function call it (with optional context), replace inline; if an array or object, recurse. Mind cycles with a `WeakSet`; mind async (await thenables) and class instances (don't recurse into them).
Walk the input. For each value: function → call → use result; object/array → recurse; primitive → keep.
Sync version
function resolve(input, ctx) {
if (input == null) return input;
if (typeof input === "function") return input(ctx);
if (Array.isArray(input)) {
return input.map((v) => resolve(v, ctx));
}
if (isPlainObject(input)) {
const out = {};
for (const k of Object.keys(input)) out[k] = resolve(input[k], ctx);
return out;
}
return input; // primitives, class instances, Date, etc.
}
function isPlainObject(v) {
if (v === null || typeof v !== "object") return false;
const p = Object.getPrototypeOf(v);
return p === null || p === Object.prototype;
}Async version
async function resolveAsync(input, ctx) {
if (input == null) return input;
if (typeof input === "function") return await input(ctx);
if (Array.isArray(input)) {
return await Promise.all(input.map((v) => resolveAsync(v, ctx)));
}
if (isPlainObject(input)) {
const entries = await Promise.all(
Object.entries(input).map(async ([k, v]) => [k, await resolveAsync(v, ctx)]),
);
return Object.fromEntries(entries);
}
return input;
}Promise.all parallelizes sibling work — same-depth functions resolve concurrently.
Cycles
If the input may self-reference (rare for config-like data but possible):
function resolve(input, ctx, seen = new WeakSet()) {
if (typeof input === "object" && input !== null) {
if (seen.has(input)) return input; // or throw
seen.add(input);
}
// ... same as above
}Edge cases
- Class instances (Date, Map, Set, custom classes) — don't recurse into them; treat as opaque.
- Functions returning functions — decide policy (call once, or keep calling until non-function).
thisbinding — passctxand let callers use arrows, or call with a target context.- Mutability — return new objects/arrays; never mutate input.
Usage example
const config = {
apiUrl: () => process.env.API_URL,
user: {
id: () => currentUser.id,
name: "Static",
},
filters: [() => Date.now()],
};
resolve(config);
// { apiUrl: "https://...", user: { id: 42, name: "Static" }, filters: [1700000000] }Interview framing
"Recursive walk: function → call, array/object → recurse, primitive → keep. isPlainObject guard prevents recursing into class instances (Date, Map). Async variant uses Promise.all to parallelize sibling work. Handle cycles with a WeakSet if the data might self-reference. Decide upfront whether to recursively resolve a function's return value or stop at one level — depends on the use case."
Follow-up questions
- •How would you handle async functions?
- •What's the policy if a function returns another function?
- •How would you support cycle detection?
Common mistakes
- •Recursing into class instances (Date/Map breaks).
- •Mutating input.
- •No async story — Promises returned by functions never awaited.
Performance considerations
- •O(n) in input size. Async version parallelizes sibling work. Deep nests can stack-overflow recursive impl — convert to iterative if user-controlled depth.
Edge cases
- •Cycles.
- •Symbol keys.
- •Getters that throw.
- •Sparse arrays.
Real-world examples
- •Config resolvers, GraphQL lazy fields, Storybook arg resolution, theme tokens that pull from CSS vars.