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.

Easy recursion object/array traversal immutability

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

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.

Easy2 cases · nesting, arrays and immutability

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.

See all 24 questions