Skip to the code
cf.

Compact an object

Write compactObject(value). It takes a plain object or an array and returns a copy without the gaps, at every depth, the way you'd clean up form data or an API payload before sending it.

Drop every object key and array item whose value is null or undefined. The array items that stay move up to close the gaps, so the array gets shorter.

Also drop every plain object or array that's empty, including one that only becomes empty because everything inside it was dropped. The top level is never dropped: if nothing is left in it, return {} or [].

Keep everything else. 0, "", false, and NaN are values, not gaps. Walk only into arrays and plain objects (the kind {} and JSON.parse make): a date, a Map, a class instance, or a function is kept as it is, even when it holds nothing.

Don't change value: every plain object and array in the result is new.

compactObject({
  name: "Ada",
  email: null,
  age: 0,
  tags: ["math", undefined, null, "code"],
  address: { street: undefined, city: null },
  settings: { theme: { color: null }, alerts: [] },
});
// { name: "Ada", age: 0, tags: ["math", "code"] }

What the tests check

  1. drops keys whose value is null or undefined
    expect(compactObject({ a: 1, b: null, c: undefined, d: "x" })).toEqual({ a: 1, d: "x" });
    
  2. drops null and undefined array items and closes the gaps
    const result = compactObject([1, null, 2, undefined, 3]);
    expect(result).toEqual([1, 2, 3]);
    expect(result).toHaveLength(3);
    
  3. keeps falsy values that aren't null or undefined
    expect(compactObject({ a: 0, b: "", c: false, d: NaN })).toEqual({ a: 0, b: "", c: false, d: NaN });
    expect(compactObject([0, "", false])).toEqual([0, "", false]);
    
  4. drops empty objects and arrays
    expect(compactObject({ a: {}, b: [], c: 1 })).toEqual({ c: 1 });
    expect(compactObject([[], {}, "x"])).toEqual(["x"]);
    
  5. drops objects and arrays that become empty, at every depth
    expect(compactObject({ a: { b: { c: null } }, d: [[null], {}], e: 1 })).toEqual({ e: 1 });
    expect(compactObject({ list: [{ a: null, b: 1 }, { c: undefined }, [[], [null], [2]]] })).toEqual({ list: [{ b: 1 }, [[2]]] });
    
  6. returns an empty object or array when nothing is left
    expect(compactObject({ a: null, b: { c: [] } })).toEqual({});
    const list = compactObject([null, [undefined]]);
    expect(Array.isArray(list)).toBe(true);
    expect(list).toEqual([]);
    
  7. keeps dates, Maps, class instances, and functions as they are
    class Empty {}
    const when = new Date(0);
    const map = new Map();
    const instance = new Empty();
    const fn = () => {};
    const result = compactObject({ when, map, instance, fn });
    expect(Object.keys(result).sort()).toEqual(["fn", "instance", "map", "when"]);
    expect(result.when).toBe(when);
    expect(result.map).toBe(map);
    expect(result.instance).toBe(instance);
    expect(result.fn).toBe(fn);
    
  8. returns new objects and arrays and leaves the input alone
    const input = { user: { name: "Ada", email: null }, tags: ["a", null], keep: { x: 1 } };
    const result = compactObject(input);
    expect(result).toEqual({ user: { name: "Ada" }, tags: ["a"], keep: { x: 1 } });
    expect(result).not.toBe(input);
    expect(result.keep).not.toBe(input.keep);
    expect(input).toEqual({ user: { name: "Ada", email: null }, tags: ["a", null], keep: { x: 1 } });
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).