Deep omit keys from an object
Deep omit rebuilds a value without the named keys, at every level of nesting. Recurse through objects skipping the keys to remove, map over arrays so objects inside them are cleaned too, and return primitives unchanged. The result is a new structure; the input is untouched.
The question
The realistic framing is redaction: strip password, token and
ssn from an object before logging it. What the interviewer watches for is whether
you remember that the sensitive keys can be nested inside arrays too, which is where most
naive implementations leak.
The solution
A Set for the keys so membership is constant time, and an inner walk
so the set is built once rather than on every recursive call.
function deepOmit(value, keys) {
const removed = new Set(keys);
function walk(current) {
if (Array.isArray(current)) return current.map(walk);
if (current === null || typeof current !== "object") return current;
const result = {};
for (const key of Object.keys(current)) {
if (removed.has(key)) continue;
result[key] = walk(current[key]);
}
return result;
}
return walk(value);
}
How it works
- Arrays are checked first, and mapped rather than rebuilt as objects, so objects inside arrays get cleaned and the array stays an array.
- Primitives return as they’re, which is the base case that terminates the recursion.
- Removed keys are skipped before recursing, so nothing under a removed key is walked at all.
- A new object at every level, so the input is never modified. One of the tests checks the returned object isn’t the same reference.
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 2 cases Practice Pad runs against your solution, and the implementation above passes all of them.
✓ removes matching keys at every depth, including inside arrays
const account = {
id: 1,
profile: { name: "Ada", password: "secret" },
teams: [{ name: "Compiler", password: "hidden" }],
};
expectEqual(deepOmit(account, ["password"]), {
id: 1,
profile: { name: "Ada" },
teams: [{ name: "Compiler" }],
});
✓ returns a copy rather than the object it was given
const source = { a: 1, drop: 2 };
const result = deepOmit(source, ["drop"]);
expectEqual(result, { a: 1 });
expectEqual(result === source, false);
Edge cases to watch out for
Forgetting objects nested inside arrays
The most common leak. Handling only plain objects means { teams: [{ password: "..." }] } keeps the password, which is exactly what a redaction helper must not do.
Turning arrays into objects
Rebuilding every non-primitive with {} and a key loop converts ["a"] into { "0": "a" }, silently breaking every array method downstream.
Mutating the input with delete
delete obj[key] on the original is faster and destroys the caller's data. If the caller wanted that they would have written it themselves.
Recursing into null
typeof null === "object" again, so an unguarded object branch tries to enumerate null and throws.
Follow up questions
How would you do the opposite and keep only certain keys?
The same walk with the condition inverted: keep a key when it’s in the set rather than skipping it. Picking is the safer default for redaction, because a new sensitive field added upstream is excluded automatically, whereas an omit list silently starts leaking it.
Why not just use delete?
delete mutates the object you were given, which surprises the caller and is a real bug when the object came from a store or a cache. It also only removes the top-level key, so nested copies survive.
How do you handle arrays during a deep omit?
Map over the array and recurse into each element, so objects inside it get cleaned. Rebuilding an array with an object literal is a common bug that converts it into an object with numeric keys.
Practise this with the tests running
Practice Pad runs these 2 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.