Topics
v6.0.0
On this page · sections

JavaScript Array & Object Utilities

Can you flatten, group, chunk, and reshape data — without tripping on holes, duplicates, or a __proto__ key?

Level: Easy · Coding Round · Data Transforms

Why This Question Matters

  • These are the utilities you write on a whiteboard and then ship in a form lib.
  • Interviewers care about holes, first-wins vs last-wins, and path collisions, not lodash names.
  • Sparse arrays and prototype pollution are the senior traps after the happy path.
  • Native flat() exists — the round is still about the contract.

Five problems. Same traversal mindset.

Problem 1 — Flatten

Question: Implement flatten(input, depth).

Description: Flatten nested arrays to a given depth. Default depth is fully flatten (Infinity). Each nested array consumes one level. Non-arrays stay as values. Skip sparse holes with i in items — a real undefined is not a hole. Match native flat() on missing indices. Unbounded recursion can blow the call stack; an explicit stack is the follow-up.

function flatten(input, depth) {}

Example

const input = [1, [2, [3, [4]]], 5]; flatten(input); // [1, 2, 3, 4, 5] flatten(input, 1); // [1, 2, [3, [4]], 5]
array ├── value → push └── array → recurse(depth - 1)

Solution

function flatten(input, depth = Infinity) { const result = []; function walk(items, remaining) { for (let i = 0; i < items.length; i++) { if (!(i in items)) continue; const item = items[i]; if (Array.isArray(item) && remaining > 0) { walk(item, remaining - 1); } else { result.push(item); } } } walk(input, depth); return result; }

Iterative (no call-stack overflow): keep frames of items, index, and remaining depth on your own stack. Same left-to-right order. No final reverse().

Use case

Tree-shaped API payloads and recursive UI data becoming a list.

Follow-ups

Q. Native flat() in production?

Answer: Yes, when the contract is enough. Write it by hand to show depth and holes.

Q. for...of on a sparse array?

Answer: Visits holes as undefined. i in items skips them.

Problem 2 — groupBy and indexBy

Question: Implement groupBy(array, keyOrFn) and indexBy(array, keyOrFn).

Description: Both take a property name or a function. Key order is first-seen. groupBy returns a Map of arrays and keeps every duplicate. indexBy returns a Map of one item per key — last write wins. Use Map so 1 and "1" stay different keys and you avoid prototype hazards.

function groupBy(array, keyOrFn) {} function indexBy(array, keyOrFn) {}

Example

groupBy(users, "role"); indexBy(users, "id");
UtilityDuplicate keysResult
groupByKeep allMap of arrays
indexByLast winsMap of items

Solution

function resolveKey(item, keyOrFn) { return typeof keyOrFn === "function" ? keyOrFn(item) : item[keyOrFn]; } function groupBy(array, keyOrFn) { const groups = new Map(); for (const item of array) { const key = resolveKey(item, keyOrFn); const bucket = groups.get(key); if (bucket) { bucket.push(item); } else { groups.set(key, [item]); } } return groups; } function indexBy(array, keyOrFn) { const result = new Map(); for (const item of array) { result.set(resolveKey(item, keyOrFn), item); } return result; }

Plain objects stringify keys. Map does not.

Use case

Users by role. Records by id for O(1) lookup.

Follow-ups

Q. Why not {}?

Answer: obj[1] and obj["1"] collide. Prototype keys are a hazard.

Problem 3 — Chunk and partition

Question: Implement chunk(array, size) and partition(array, predicate).

Description: chunk splits into consecutive runs of size. The last chunk may be shorter. size must be a positive integer or throw — 0 is an infinite loop. partition returns [pass, fail] in one pass; the predicate runs once per item. Two filter calls walk twice.

function chunk(array, size) {} function partition(array, predicate) {}

Example

chunk([1, 2, 3, 4, 5], 2); // [[1, 2], [3, 4], [5]] partition([1, 2, 3, 4], (n) => n % 2 === 0); // [[2, 4], [1, 3]]

Solution

function chunk(array, size) { if (!Number.isInteger(size) || size < 1) { throw new RangeError("chunk size must be a positive integer"); } const result = []; for (let i = 0; i < array.length; i += size) { result.push(array.slice(i, i + size)); } return result; } function partition(array, predicate) { const pass = []; const fail = []; for (const item of array) { if (predicate(item)) { pass.push(item); } else { fail.push(item); } } return [pass, fail]; }

Use case

Batch uploads. Split selected vs rest in a table.

Problem 4 — Unique

Question: Implement unique(array) and uniqueBy(array, keyOrFn).

Description: Drop duplicates, keep first occurrence, preserve order. unique is for primitives (Set, SameValueZero: NaN collapses, +0 and -0 are the same). uniqueBy keys objects by property or function. First-wins is the default contract; last-wins is a reverse scan or indexBy. Document which one you chose.

function unique(array) {} function uniqueBy(array, keyOrFn) {}

Example

unique([1, 2, 2, 3, 1]); // [1, 2, 3] uniqueBy( [ { id: 1, name: "Ada" }, { id: 2, name: "Grace" }, { id: 1, name: "Duplicate" }, ], "id" ); // Ada, Grace

Solution

function unique(array) { return [...new Set(array)]; } function uniqueBy(array, keyOrFn) { const seen = new Set(); const result = []; for (const item of array) { const key = typeof keyOrFn === "function" ? keyOrFn(item) : item[keyOrFn]; if (seen.has(key)) continue; seen.add(key); result.push(item); } return result; }

Use case

Unique filter chips. Merge API rows by id.

Follow-ups

Q. First-wins vs last-wins?

Answer: A contract. Document it. Do not leave it as an accident of Map.set.

Problem 5 — Flatten and unflatten objects

Question: Implement flattenObject(obj) and unflattenObject(flat).

Description: Flatten nested plain objects into dot-path keys. Recurse only into non-empty plain objects — arrays, Date, and empty objects stay leaves. Unflatten walks each path and creates missing objects. If a path needs the same key as both a leaf and an object, throw. Never assign __proto__, constructor, or prototype.

function flattenObject(obj) {} function unflattenObject(flat) {}

Example

flattenObject({ user: { name: "Ada", address: { city: "London" } }, tags: ["js"], settings: {}, }); // { // "user.name": "Ada", // "user.address.city": "London", // tags: ["js"], // settings: {}, // }
"user.address.city" ↓ user → address → city

Solution

function isPlainObject(value) { if (typeof value !== "object" || value === null) { return false; } const proto = Object.getPrototypeOf(value); return proto === Object.prototype || proto === null; } const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]); function flattenObject(obj, prefix = "", result = {}) { for (const key of Object.keys(obj)) { if (UNSAFE_KEYS.has(key)) continue; const path = prefix ? `${prefix}.${key}` : key; const value = obj[key]; const shouldRecurse = isPlainObject(value) && Object.keys(value).length > 0; if (shouldRecurse) { flattenObject(value, path, result); } else { result[path] = value; } } return result; } function unflattenObject(flat) { const result = {}; for (const flatKey of Object.keys(flat)) { const segments = flatKey.split("."); let current = result; for (let i = 0; i < segments.length; i++) { const segment = segments[i]; if (UNSAFE_KEYS.has(segment)) { throw new Error(`Unsafe key segment: ${segment}`); } const isLast = i === segments.length - 1; if (isLast) { if (isPlainObject(current[segment])) { throw new Error(`Collision at key: ${flatKey}`); } current[segment] = flat[flatKey]; } else { if (current[segment] === undefined) { current[segment] = {}; } else if (!isPlainObject(current[segment])) { throw new Error(`Collision at key: ${flatKey}`); } current = current[segment]; } } } return result; }

Use case

Form field paths. Config editors. Query builders.

Follow-ups

Q. Why throw on collision?

Answer: Silent overwrite looks like valid state. The contract should fail.

Complexity

UtilityTimeSpace
flattenO(n)O(n) + O(d) stack
groupBy / indexBy / unique*O(n)O(n)
chunk / partitionO(n)O(n)
flatten / unflatten objectsO(k)O(k)

Do not mutate the input unless the interviewer says so.

Senior Engineer Interview Answer

“Flatten is depth-limited DFS and I skip holes with i in items. Grouping uses a Map so keys stay identity. Chunk validates size. Unique is first-wins unless we say otherwise. Object paths recurse only into plain objects, throw on leaf/object collisions, and never write __proto__.”

Interview Checklist

✓ depth + holes on flatten ✓ Map not {} ✓ chunk size check ✓ first-wins vs last-wins ✓ empty object stays a leaf ✓ path collisions throw ✓ unsafe keys

Traversal plus a contract. Native APIs when they already match.

Last updated on