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
lodashnames. - 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");| Utility | Duplicate keys | Result |
|---|---|---|
groupBy | Keep all | Map of arrays |
indexBy | Last wins | Map 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, GraceSolution
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 → citySolution
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
| Utility | Time | Space |
|---|---|---|
flatten | O(n) | O(n) + O(d) stack |
groupBy / indexBy / unique* | O(n) | O(n) |
chunk / partition | O(n) | O(n) |
| flatten / unflatten objects | O(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 keysTraversal plus a contract. Native APIs when they already match.