Filter an array of objects
Apply each supplied option as its own filter, one after another, skipping any that were not provided. The design decision that matters is order: when records are merged before filtering, the filters see combined totals, which produces a materially different result from filtering first.
The question
The open-ended one. There’s no single correct answer, which is the point: the interviewer wants to see how you structure code when the requirements arrive as a bag of optional flags. The question that earns the most credit is one you ask them: should merging happen before or after filtering?
The solution
Each filter is independent and skipped when its option is absent, so adding a new criterion means adding one block and nothing else.
function selectData(sessions, options = {}) {
// Merge first when asked, so the filters see combined totals rather than
// individual visits. Never mutate the input.
let rows = options.merge ? mergeSessions(sessions) : sessions.map((session) => ({ ...session }));
if (options.user !== undefined) {
rows = rows.filter((row) => row.user === options.user);
}
if (options.minDuration !== undefined) {
rows = rows.filter((row) => row.duration >= options.minDuration);
}
if (options.equipment) {
rows = rows.filter((row) => options.equipment.some((item) => row.equipment.includes(item)));
}
return rows;
}
function mergeSessions(sessions) {
const byUser = new Map();
for (const session of sessions) {
if (!byUser.has(session.user)) {
byUser.set(session.user, { user: session.user, duration: 0, equipment: new Set() });
}
const merged = byUser.get(session.user);
merged.duration += session.duration;
for (const item of session.equipment) merged.equipment.add(item);
}
return [...byUser.values()].map((merged) => ({
user: merged.user,
duration: merged.duration,
equipment: [...merged.equipment].sort(),
}));
}
How it works
- Merging happens first when requested, so
minDurationis tested against a user's combined total rather than a single visit. Filtering first would give a different and equally defensible answer. options.user !== undefined, not truthiness, because a user id of0is a legitimate value that a falsy check would silently ignore.- Filters compose by reassignment, each narrowing the previous result, which keeps every criterion independent and easy to add to.
- The input is copied when not merging, so callers never see their records modified.
The tests it has to pass
This is the part most explanations leave out. "It works" is not a claim you can check by reading. These are the 5 cases Practice Pad runs against your solution, and the implementation above passes all of them.
✓ filters sessions by user
const sessions = [
{ user: 7, duration: 150, equipment: ["dumbbell"] },
{ user: 1, duration: 40, equipment: ["bench"] },
{ user: 7, duration: 100, equipment: ["bike"] },
];
expectEqual(selectData(sessions, { user: 7 }), [
{ user: 7, duration: 150, equipment: ["dumbbell"] },
{ user: 7, duration: 100, equipment: ["bike"] },
]);
✓ filters sessions by minimum duration
const sessions = [
{ user: 7, duration: 150, equipment: ["dumbbell"] },
{ user: 1, duration: 40, equipment: ["bench"] },
];
expectEqual(selectData(sessions, { minDuration: 100 }), [
{ user: 7, duration: 150, equipment: ["dumbbell"] },
]);
✓ keeps sessions that use any requested equipment
const sessions = [
{ user: 7, duration: 100, equipment: ["bike"] },
{ user: 1, duration: 40, equipment: ["bench"] },
{ user: 2, duration: 200, equipment: ["bike"] },
];
expectEqual(selectData(sessions, { equipment: ["bike"] }), [
{ user: 7, duration: 100, equipment: ["bike"] },
{ user: 2, duration: 200, equipment: ["bike"] },
]);
✓ merges users before filtering, combining durations and equipment
const sessions = [
{ user: 7, duration: 150, equipment: ["dumbbell"] },
{ user: 1, duration: 40, equipment: ["bench"] },
{ user: 7, duration: 100, equipment: ["bike"] },
{ user: 2, duration: 200, equipment: ["bike"] },
];
const merged = selectData(sessions, { merge: true, minDuration: 200 });
const byUser = [...merged].sort((a, b) => a.user - b.user);
expectEqual(byUser, [
{ user: 2, duration: 200, equipment: ["bike"] },
{ user: 7, duration: 250, equipment: ["bike", "dumbbell"] },
]);
✓ leaves the original sessions unchanged
const sessions = [
{ user: 7, duration: 150, equipment: ["dumbbell"] },
{ user: 7, duration: 100, equipment: ["bike"] },
];
selectData(sessions, { merge: true });
expectEqual(sessions.map((session) => session.duration), [150, 100]);
Edge cases to watch out for
Using truthiness to detect supplied options
if (options.user) ignores a user id of 0, and if (options.minDuration) ignores a minimum of 0. Compare against undefined instead.
Filtering before merging without saying so
Both orders are defensible and they produce different results. Picking one silently is the actual mistake; stating the choice and its consequence is what the interviewer is listening for.
Returning the caller's objects
filter returns a new array of the same object references, so a later mutation of a result also mutates the input. Copying the records keeps the boundary clean.
Nesting the conditions into one predicate
A single filter with four ANDed conditions and undefined guards inside it works, and becomes unreadable at the fifth criterion. Separate passes cost a little performance and keep each rule isolated.
rows.filter((row) => predicates.every((p) => p(row))). That’s one pass instead of several, and new criteria become one entry in a table rather than another block of code.Follow up questions
How would you add sorting and pagination?
Keep them as separate stages after filtering, in that order: filter, then sort, then slice. Sorting before filtering wastes work on rows about to be discarded, and paginating before sorting returns the wrong page entirely. Copy before sorting, since sort mutates.
Should you merge before or after filtering?
It depends what the filter means, and it changes the answer. Filtering after merging tests a user's combined total; filtering before tests individual records and merges only what survived. Neither is wrong, and choosing without saying which you chose is.
Why check against undefined instead of truthiness?
Because 0, "" and false are all legitimate filter values that a truthiness check treats as absent. A minimum duration of zero or a user id of zero would be silently ignored.
Practise this with the tests running
Practice Pad runs these 5 cases against your own solution, in a real editor, on your Mac. Twenty-four questions, and an optional AI panel that explains why a test failed.
Download for macOS Free. Requires macOS 12 or later on Apple Silicon.