Skip to the code
cf.

Set by path

Write set(object, path, value). It writes value at path inside object, creating whatever is missing on the way. It changes object in place and returns it.

path is a string like "a.b[0].c", or an array of keys like ["a", "b", 0, "c"]. Objects already on the path are kept and written into. When a step is missing or holds something that isn't an object (a number, null), put a new container there: an array if the next key is an array index (digits only, like 0 or "12"), and a plain object otherwise.

Paths can come from user input, so guard against prototype pollution: if any key in the path is __proto__, constructor, or prototype, change nothing and return object.

set({}, "user.tags[0]", "js");    // { user: { tags: ["js"] } }
set({ a: { x: 1 } }, "a.y", 2);   // { a: { x: 1, y: 2 } }
set({ a: 1 }, ["a", "b"], 2);     // { a: { b: 2 } }
set({}, "__proto__.admin", true); // {}, and Object.prototype is untouched

What the tests check

  1. creates nested objects along the path
    expect(set({}, "a.b.c", 1)).toEqual({ a: { b: { c: 1 } } });
    
  2. creates arrays where the next key is an index
    expect(set({}, "list[0].name", "x")).toEqual({ list: [{ name: "x" }] });
    
  3. accepts an array of keys, with numbers as indexes
    expect(set({}, ["a", 0, "b"], 1)).toEqual({ a: [{ b: 1 }] });
    
  4. only digits-only keys make arrays
    expect(set({}, "a.1x", 1)).toEqual({ a: { "1x": 1 } });
    expect(set({}, ["b", "-1"], 2)).toEqual({ b: { "-1": 2 } });
    
  5. keeps and writes into objects already on the path
    const inner = { x: 1 };
    const o = { a: inner, other: true };
    set(o, "a.y", 2);
    expect(o.a).toBe(inner);
    expect(o).toEqual({ a: { x: 1, y: 2 }, other: true });
    
  6. replaces a value in the way that isn't an object
    expect(set({ a: 1 }, "a.b", 2)).toEqual({ a: { b: 2 } });
    expect(set({ a: null }, "a[0]", "x")).toEqual({ a: ["x"] });
    
  7. changes the object in place and returns it
    const o = { keep: true };
    expect(set(o, "added", 1)).toBe(o);
    expect(o).toEqual({ keep: true, added: 1 });
    
  8. refuses keys that could pollute prototypes, changing nothing
    const target = {};
    try {
      set(target, "__proto__.polluted", true);
      set(target, ["constructor", "prototype", "polluted"], true);
      set(target, "a.__proto__.polluted", true);
      expect(({}).polluted).toBeUndefined();
      expect(target).toEqual({});
    } finally {
      delete Object.prototype.polluted;
    }
    

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).