Get and Set Nested Object Paths
Implement get(obj, path, fallback) and set(obj, path, value) for dot paths. Missing segments, non-object middles, and mutating vs cloning.
- javascript
- objects
- interview
- lodash
user.address.city throws when address is missing. Optional chaining returns undefined and cannot write. Config code still needs get(obj, 'a.b.c', fallback) and set(obj, 'a.b.c', value) that walk a path string without exploding on a hole in the middle.
The interview is not whether you can split('.'). It is what you do when a segment is missing, when the middle value is a number or null, and whether set mutates the input. Those three decisions are the API. Optional chaining is the language’s read-only cousin; this page is the walk you write by hand.
getwalks segments and returnsfallbackwhen the walk dies.undefinedat the leaf usesfallbacktoo;0and''do not.setmust decide: create missing objects, throw, or no-op when a middle is not a container.- Mutating
setedits the input. An immutablesetcopies only the path it touches.- Path is a convention (
'a.b'or['a','b']). Dots inside keys need the array form.- Don’t treat
nullas an object.typeof null === 'object'is the trap.
Walk, and decide what happens when the path dies
Normalize the path to an array of keys once. Accept both 'a.b.c' and ['a', 'b', 'c']. Empty segments from 'a..b' are real keys named ''; don’t filter(Boolean) them away unless you say you are collapsing dots.
get
function get(obj, path, fallback) {
const keys = Array.isArray(path) ? path : String(path).split('.');
if (keys.length === 1 && keys[0] === '' && path === '') {
return obj === undefined ? fallback : obj;
}
let cur = obj;
for (const key of keys) {
if (cur == null || (typeof cur !== 'object' && typeof cur !== 'function')) {
return fallback;
}
cur = cur[key];
}
return cur === undefined ? fallback : cur;
}
const data = { a: { b: { c: 1, z: 0 } } };
get(data, 'a.b.c'); // 1
get(data, 'a.b.missing', 42); // 42
get(data, 'a.b.z', 42); // 0 (valid falsy)
get(data, 'a.x.y', 42); // 42, no throw
get({ a: null }, 'a.b', 42); // 42
cur == null catches null and undefined. A number, string, or boolean in the middle also dies: get({ a: 1 }, 'a.toFixed') would otherwise pick up inherited methods if you kept walking. For config bags you usually want own-data walks, not Number.prototype.
fallback only replaces undefined, not null. get({ a: null }, 'a', 42) returns null. If you want nullish, compare with == null at the leaf and say so. Silent ?? behavior surprises callers who stored null on purpose.
Inherited keys: cur[key] reads the prototype. Use Object.hasOwn if the prompt wants own properties only. Own-only get treats toString on a plain object as missing.
set that mutates
function set(obj, path, value) {
if (obj == null || (typeof obj !== 'object' && typeof obj !== 'function')) {
throw new TypeError('set target must be an object');
}
const keys = Array.isArray(path) ? path : String(path).split('.');
if (keys.length === 0) return obj;
let cur = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
const next = cur[key];
if (next == null || typeof next !== 'object') {
cur[key] = {};
}
cur = cur[key];
}
cur[keys[keys.length - 1]] = value;
return obj;
}
const box = {};
set(box, 'a.b.c', 1);
box.a.b.c; // 1
When the path dies, this version overwrites a non-object middle with {}. set({ a: 1 }, 'a.b', 2) becomes { a: { b: 2 } } and the 1 is gone. That is hostile if a was money. The other legal choices:
Middle is 1 / null / missing |
set behavior |
|---|---|
| Create | replace with {}, keep walking (above) |
| Throw | TypeError('cannot set into a non-object') |
| No-op | leave the input, return it unchanged |
Create-as-you-go is what most whiteboard prompts expect. Throw is safer for untrusted paths. Pick one in the first sentence.
Immutable set
Mutating is wrong next to React state and next to a shared config object. Copy along the path; leave sibling keys on shared structure. That is the same idea as immutability patterns and deep vs shallow copy: you do not clone the world.
function setImmutable(obj, path, value) {
const keys = Array.isArray(path) ? path : String(path).split('.');
function branch(src, i) {
const key = keys[i];
const clone = Array.isArray(src)
? src.slice()
: Object.assign({}, src != null && typeof src === 'object' ? src : {});
clone[key] =
i === keys.length - 1 ? value : branch(src?.[key], i + 1);
return clone;
}
if (keys.length === 0) return obj;
return branch(obj, 0);
}
const original = { a: { b: 1, c: 2 } };
const next = setImmutable(original, 'a.b', 9);
original.a.b; // 1
next.a.b; // 9
next.a.c; // 2
next.a === original.a; // false
Numeric segments ('items.0.name') become string keys on objects. If you want real arrays, detect ^\d+$ and allocate [] instead of {}. That is a product decision, the same family as flatten / unflatten objects. Don’t silently turn { '0': 'x' } into an array unless the prompt showed arrays.
Footguns in nested get and set
typeof null === 'object'. Walking intonullthrows if you only checktypeof === 'object'. Use== nullfirst.- Falsy leaves.
0,false,''are values. Fallback is forundefined(and maybe missing keys), not for falsy. - Prototype keys.
get({}, 'toString')is a function unless youhasOwn. - Dots in names.
'a.b'cannot name a key that contains a dot. Offer the array path. - Frozen / sealed objects. Mutating
setthrowsTypeError. Immutablesetstill works if you copy. - Arrays as middles. Overwriting an array with
{}becausetypeofis object but you assigned a non-index key is usually fine; overwriting it because the next key is'0'should keep the array.
get is a read. It should not create. A get that fills in {} as it walks is a set in disguise and will mutate objects during render.
Interview out loud
“I split a dot path, or accept an array of keys, and walk. get returns the fallback when a segment is missing, when the current value is nullish, or when the leaf is undefined. Zero and empty string are real values. set either mutates and creates intermediate objects, or copies the path for an immutable version; I will ask which. If a middle is a number or null, I overwrite with an object on the mutating helper and I will switch to throw if they want strict paths. I do not use optional chaining for the write, and I do not filter empty segments unless we agree dots collapse.”
Further reading
Related on this site
- Optional chaining
- Flatten and unflatten objects
- Immutability patterns in JS
- Deep vs shallow copy
- Deep equality in JavaScript
- Getters and setters
Related guides
- Deep Clone ImplementationInterview deep clone: JSON is a lossy subset, structuredClone is the host, and recursion plus a WeakMap is how you survive cycles.
- Deep Equality in JavaScriptCompare values structurally: primitives, NaN, arrays, plain objects, dates, and the cycle that will infinite-loop a naive recursion.
- Flatten and Unflatten ObjectsTurn nested objects into dot-path keys and back. Arrays vs objects, numeric keys, and why round-tripping `a.b[0]` is a product decision.
- Object.assign PolyfillCopy enumerable own properties onto a target. Getters that run, symbols, null/undefined sources skipped, and the mutation-in-place return value.
- Object.entries and fromEntriesRound-trip own enumerable string keys with Object.entries and Object.fromEntries. Not a Map, not symbols, and later duplicate keys win.