Implement unsquashObject(flat) — the inverse of squash-object. You're given a single-level object whose keys are dot-delimited paths, and you rebuild the nested object they describe. unsquashObject({ 'a.b': 1 }) becomes { a: { b: 1 } }. Think of it as turning a flat list of full file paths back into a folder tree: docs/notes/todo.txt becomes a docs folder containing a notes folder containing todo.txt. This is the expansion that config loaders and form libraries do to turn a flat user.address.city key back into a real nested shape. Keep it symmetric with squashObject so a round-trip is lossless.
// flat: a single-level object. Every key is a dot-joined path string;
// every value is a LEAF — a primitive, null, an array, or an object,
// kept exactly as-is (never re-split or recursed into).
// returns: a new nested object whose structure is described by the dotted keys.
function unsquashObject(flat): Record<string, unknown>;
// One dotted key becomes one level of nesting.
unsquashObject({ 'a.b': 1 });
// → { a: { b: 1 } }
// A deep path, a shorter path that shares its prefix, and a flat key.
unsquashObject({ 'a.b.c': 1, 'a.d': 2, e: 3 });
// → { a: { b: { c: 1 }, d: 2 }, e: 3 }
// Leaves are stored as-is — an array is never treated as a branch.
unsquashObject({ 'tags': ['x', 'y'], 'meta.author': null });
// → { tags: ['x', 'y'], meta: { author: null } }
. and walk the path. For every entry, break the key into segments. Every segment except the last names an intermediate object to step into (creating it if it isn't there yet); the last segment names where the value lands.{ e: 3 } has a one-segment path, so it becomes { e: 3 } — no nesting.{ 'a.b': 1, 'a.c': 2 } builds a single a object holding both b and c. The second key must reuse the a you already created, not clobber it.null, an object, or a primitive is set verbatim at the final segment. Don't recurse into it; don't re-split its own keys on dots. This mirrors squashObject's leaf rule so the two round-trip cleanly.{ a: 1, 'a.b': 2 } — the key that comes later decides. Here 'a.b' overwrites the leaf 1, giving { a: { b: 2 } }. Reverse the order and the later leaf wins instead.flat; build and return a fresh nested object.You'll take each dotted key, split it into segments, and walk down a result object — creating an object for every segment but the last, where you drop the value.
Imagine a flat list of full file paths: docs/notes/todo.txt → <contents>. You want the folder tree back — a docs folder holding a notes folder holding the todo.txt file. unsquashObject does exactly that to an object, using . as the separator instead of /. For each entry, you read the key, chop it at every dot, and follow that chain into the result: each segment names a level to step into (and to create if it isn't there yet), and the final segment is where the value gets set. It's the inverse of squashObject, which collapsed the tree into dotted keys; here you grow the tree back from them.
Hold two things in your head: the one result object you're growing, and a cursor — the node you're currently standing on as you walk a path. For each key, you split it into segments and walk: at every segment except the last, you make sure there's an object there and step into it; at the last segment, you set the value. The split does the hard part — 'b.d.e'.split('.') gives you ['b', 'd', 'e'], and the only question left is "for each segment, am I at the end of the path or not?" Segments before the last are containers to descend through; the last segment is the slot the value goes in.
The shape is right — split, walk, set — but the first version usually creates a fresh object at every intermediate segment without checking whether one is already there:
function unsquashObject(flat) {
const result = {};
for (const key of Object.keys(flat)) {
const segments = key.split('.');
let node = result;
for (let i = 0; i < segments.length - 1; i++) {
node[segments[i]] = {}; // always overwrite with a fresh object
node = node[segments[i]];
}
node[segments[segments.length - 1]] = flat[key];
}
return result;
}
This works for a single key, then loses data the moment two keys share a prefix. Take { 'a.b': 1, 'a.c': 2 }. The first key builds result.a = { b: 1 }. The second key walks a again — but node['a'] = {} throws away the { b: 1 } you just built and replaces it with an empty object, so b is gone and you end up with { a: { c: 2 } }. The fix is to reuse an existing object at a segment instead of always clobbering it.
function unsquashObject(flat) {
const result = {};
for (const key of Object.keys(flat)) {
const segments = key.split('.');
const value = flat[key];
// Walk down to the parent of the last segment, creating objects as we go.
let node = result;
for (let i = 0; i < segments.length - 1; i++) {
const segment = segments[i];
// If nothing is here yet, OR what's here is a leaf (a non-plain-object
// like a number or an array) left by an earlier conflicting key, replace
// it with a fresh object so we can keep descending. The later key wins.
if (
typeof node[segment] !== 'object' ||
node[segment] === null ||
Array.isArray(node[segment])
) {
node[segment] = {};
}
node = node[segment];
}
// Set the value at the final segment. The value is a LEAF — stored as-is,
// never re-split or recursed into, so a clean round-trip with squash holds.
node[segments[segments.length - 1]] = value;
}
return result;
}
module.exports = { unsquashObject };
Two ideas carry the fix. The inner loop stops one short of the end — i < segments.length - 1 — so it only ever walks the containers; the value-set happens once, after the loop, at the final segment. And the guard before node[segment] = {} is the whole difference from the naive version: it only creates a new object when there isn't already a usable one there. If a previous key already built a as an object, the guard is false and you descend into the existing a, growing it instead of wiping it. The same guard handles a conflict — if a currently holds the leaf 1, that's not a plain object, so you replace it with {} and the branch wins.
Trace unsquashObject({ 'a.b.c': 1, 'a.d': 2, e: 3 }). The result starts empty and every key writes into it.
result = {}
key 'a.b.c', value 1 → segments ['a', 'b', 'c']
i=0 'a' missing → result.a = {} ; node = result.a
i=1 'b' missing → result.a.b = {} ; node = result.a.b
(loop stops before 'c')
set node['c'] = 1
result = { a: { b: { c: 1 } } }
key 'a.d', value 2 → segments ['a', 'd']
i=0 'a' already an object → reuse it ; node = result.a
(loop stops before 'd')
set node['d'] = 2
result = { a: { b: { c: 1 }, d: 2 } }
key 'e', value 3 → segments ['e']
(loop never runs — only one segment, length - 1 is 0)
set node['e'] = 3
result = { a: { b: { c: 1 }, d: 2 }, e: 3 }
return { a: { b: { c: 1 }, d: 2 }, e: 3 }
The second key is the one that matters. Its first segment is 'a', and 'a' is already an object from the first key — so the guard is false, you skip the = {}, and you descend into the existing a. That's why b survives: you grew a rather than replacing it. The third key, 'e', has a single segment, so segments.length - 1 is 0 and the inner loop body never runs; the value is set straight onto the top-level result.
{}. This is the bug everyone hits. If you write node[segment] = {} unconditionally, the second key that shares a prefix erases the branch the first key built: { 'a.b': 1, 'a.c': 2 } comes out as { a: { c: 2 } } with b lost. Guard it — only assign {} when there isn't already an object to descend into.flat has { 'a.b': { c: 1 } }, the result is { a: { b: { c: 1 } } } and the { c: 1 } object is kept whole, not turned into another c path. Recursing here breaks the round-trip with squashObject, which treated that object's contents as already-flattened.null as branches. typeof reports 'object' for arrays and null, so a guard that checks only typeof node[segment] === 'object' would try to descend into an array left by a conflicting key. Exclude them: a usable container is an object that is not null and not an array. (Values themselves are always set as leaves; this only matters when an earlier key parked a non-object where a later key needs to descend.)i < segments.length and then also sets the value, you create an extra empty object past the leaf — 'a.b' becomes { a: { b: {} } } with the value lost inside. Stop the walk one short: loop to segments.length - 1, then set the value at the final segment after the loop.'e' splits into ['e'], a single segment. segments.length - 1 is 0, so the inner loop never runs and you set result['e'] directly. If you assume there's always at least one intermediate segment, you'll mishandle every flat key.{ a: 1, 'a.b': 2 }), something has to give. Pick a rule and make it deliberate. Here, iterating keys in order and letting each one overwrite means the later key wins — the branch replaces the leaf 1 to give { a: { b: 2 } }, and in the reverse order the later leaf replaces the branch. Don't leave it to chance.squashObject (the inverse). Takes { a: { b: 1 } } and flattens it to { 'a.b': 1 } by walking the tree and dot-joining the path to each leaf. Round-tripping unsquash(squash(obj)) returns the original for any input without dotted keys or empty branches — which is exactly the symmetry these leaf rules protect.{ 'a.b': 1 } meaning a single field named a.b, is indistinguishable from the path a → b. Real libraries (Lodash's _.set, the flat package) let you pass a different delimiter or escape dots so such keys survive. The walk is identical; only how you split the key changes.{ 'list.0.name': 'Ada' } — you'd rebuild arrays by noticing a numeric segment and creating [] instead of {} at that level, then assigning by index. Keep it opt-in; the default treats every segment as an object key so the simple round-trip stays predictable.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Implement unsquashObject(flat) — the inverse of squash-object. You're given a single-level object whose keys are dot-delimited paths, and you rebuild the nested object they describe. unsquashObject({ 'a.b': 1 }) becomes { a: { b: 1 } }. Think of it as turning a flat list of full file paths back into a folder tree: docs/notes/todo.txt becomes a docs folder containing a notes folder containing todo.txt. This is the expansion that config loaders and form libraries do to turn a flat user.address.city key back into a real nested shape. Keep it symmetric with squashObject so a round-trip is lossless.
// flat: a single-level object. Every key is a dot-joined path string;
// every value is a LEAF — a primitive, null, an array, or an object,
// kept exactly as-is (never re-split or recursed into).
// returns: a new nested object whose structure is described by the dotted keys.
function unsquashObject(flat): Record<string, unknown>;
// One dotted key becomes one level of nesting.
unsquashObject({ 'a.b': 1 });
// → { a: { b: 1 } }
// A deep path, a shorter path that shares its prefix, and a flat key.
unsquashObject({ 'a.b.c': 1, 'a.d': 2, e: 3 });
// → { a: { b: { c: 1 }, d: 2 }, e: 3 }
// Leaves are stored as-is — an array is never treated as a branch.
unsquashObject({ 'tags': ['x', 'y'], 'meta.author': null });
// → { tags: ['x', 'y'], meta: { author: null } }
. and walk the path. For every entry, break the key into segments. Every segment except the last names an intermediate object to step into (creating it if it isn't there yet); the last segment names where the value lands.{ e: 3 } has a one-segment path, so it becomes { e: 3 } — no nesting.{ 'a.b': 1, 'a.c': 2 } builds a single a object holding both b and c. The second key must reuse the a you already created, not clobber it.null, an object, or a primitive is set verbatim at the final segment. Don't recurse into it; don't re-split its own keys on dots. This mirrors squashObject's leaf rule so the two round-trip cleanly.{ a: 1, 'a.b': 2 } — the key that comes later decides. Here 'a.b' overwrites the leaf 1, giving { a: { b: 2 } }. Reverse the order and the later leaf wins instead.flat; build and return a fresh nested object.You'll take each dotted key, split it into segments, and walk down a result object — creating an object for every segment but the last, where you drop the value.
Imagine a flat list of full file paths: docs/notes/todo.txt → <contents>. You want the folder tree back — a docs folder holding a notes folder holding the todo.txt file. unsquashObject does exactly that to an object, using . as the separator instead of /. For each entry, you read the key, chop it at every dot, and follow that chain into the result: each segment names a level to step into (and to create if it isn't there yet), and the final segment is where the value gets set. It's the inverse of squashObject, which collapsed the tree into dotted keys; here you grow the tree back from them.
Hold two things in your head: the one result object you're growing, and a cursor — the node you're currently standing on as you walk a path. For each key, you split it into segments and walk: at every segment except the last, you make sure there's an object there and step into it; at the last segment, you set the value. The split does the hard part — 'b.d.e'.split('.') gives you ['b', 'd', 'e'], and the only question left is "for each segment, am I at the end of the path or not?" Segments before the last are containers to descend through; the last segment is the slot the value goes in.
The shape is right — split, walk, set — but the first version usually creates a fresh object at every intermediate segment without checking whether one is already there:
function unsquashObject(flat) {
const result = {};
for (const key of Object.keys(flat)) {
const segments = key.split('.');
let node = result;
for (let i = 0; i < segments.length - 1; i++) {
node[segments[i]] = {}; // always overwrite with a fresh object
node = node[segments[i]];
}
node[segments[segments.length - 1]] = flat[key];
}
return result;
}
This works for a single key, then loses data the moment two keys share a prefix. Take { 'a.b': 1, 'a.c': 2 }. The first key builds result.a = { b: 1 }. The second key walks a again — but node['a'] = {} throws away the { b: 1 } you just built and replaces it with an empty object, so b is gone and you end up with { a: { c: 2 } }. The fix is to reuse an existing object at a segment instead of always clobbering it.
function unsquashObject(flat) {
const result = {};
for (const key of Object.keys(flat)) {
const segments = key.split('.');
const value = flat[key];
// Walk down to the parent of the last segment, creating objects as we go.
let node = result;
for (let i = 0; i < segments.length - 1; i++) {
const segment = segments[i];
// If nothing is here yet, OR what's here is a leaf (a non-plain-object
// like a number or an array) left by an earlier conflicting key, replace
// it with a fresh object so we can keep descending. The later key wins.
if (
typeof node[segment] !== 'object' ||
node[segment] === null ||
Array.isArray(node[segment])
) {
node[segment] = {};
}
node = node[segment];
}
// Set the value at the final segment. The value is a LEAF — stored as-is,
// never re-split or recursed into, so a clean round-trip with squash holds.
node[segments[segments.length - 1]] = value;
}
return result;
}
module.exports = { unsquashObject };
Two ideas carry the fix. The inner loop stops one short of the end — i < segments.length - 1 — so it only ever walks the containers; the value-set happens once, after the loop, at the final segment. And the guard before node[segment] = {} is the whole difference from the naive version: it only creates a new object when there isn't already a usable one there. If a previous key already built a as an object, the guard is false and you descend into the existing a, growing it instead of wiping it. The same guard handles a conflict — if a currently holds the leaf 1, that's not a plain object, so you replace it with {} and the branch wins.
Trace unsquashObject({ 'a.b.c': 1, 'a.d': 2, e: 3 }). The result starts empty and every key writes into it.
result = {}
key 'a.b.c', value 1 → segments ['a', 'b', 'c']
i=0 'a' missing → result.a = {} ; node = result.a
i=1 'b' missing → result.a.b = {} ; node = result.a.b
(loop stops before 'c')
set node['c'] = 1
result = { a: { b: { c: 1 } } }
key 'a.d', value 2 → segments ['a', 'd']
i=0 'a' already an object → reuse it ; node = result.a
(loop stops before 'd')
set node['d'] = 2
result = { a: { b: { c: 1 }, d: 2 } }
key 'e', value 3 → segments ['e']
(loop never runs — only one segment, length - 1 is 0)
set node['e'] = 3
result = { a: { b: { c: 1 }, d: 2 }, e: 3 }
return { a: { b: { c: 1 }, d: 2 }, e: 3 }
The second key is the one that matters. Its first segment is 'a', and 'a' is already an object from the first key — so the guard is false, you skip the = {}, and you descend into the existing a. That's why b survives: you grew a rather than replacing it. The third key, 'e', has a single segment, so segments.length - 1 is 0 and the inner loop body never runs; the value is set straight onto the top-level result.
{}. This is the bug everyone hits. If you write node[segment] = {} unconditionally, the second key that shares a prefix erases the branch the first key built: { 'a.b': 1, 'a.c': 2 } comes out as { a: { c: 2 } } with b lost. Guard it — only assign {} when there isn't already an object to descend into.flat has { 'a.b': { c: 1 } }, the result is { a: { b: { c: 1 } } } and the { c: 1 } object is kept whole, not turned into another c path. Recursing here breaks the round-trip with squashObject, which treated that object's contents as already-flattened.null as branches. typeof reports 'object' for arrays and null, so a guard that checks only typeof node[segment] === 'object' would try to descend into an array left by a conflicting key. Exclude them: a usable container is an object that is not null and not an array. (Values themselves are always set as leaves; this only matters when an earlier key parked a non-object where a later key needs to descend.)i < segments.length and then also sets the value, you create an extra empty object past the leaf — 'a.b' becomes { a: { b: {} } } with the value lost inside. Stop the walk one short: loop to segments.length - 1, then set the value at the final segment after the loop.'e' splits into ['e'], a single segment. segments.length - 1 is 0, so the inner loop never runs and you set result['e'] directly. If you assume there's always at least one intermediate segment, you'll mishandle every flat key.{ a: 1, 'a.b': 2 }), something has to give. Pick a rule and make it deliberate. Here, iterating keys in order and letting each one overwrite means the later key wins — the branch replaces the leaf 1 to give { a: { b: 2 } }, and in the reverse order the later leaf replaces the branch. Don't leave it to chance.squashObject (the inverse). Takes { a: { b: 1 } } and flattens it to { 'a.b': 1 } by walking the tree and dot-joining the path to each leaf. Round-tripping unsquash(squash(obj)) returns the original for any input without dotted keys or empty branches — which is exactly the symmetry these leaf rules protect.{ 'a.b': 1 } meaning a single field named a.b, is indistinguishable from the path a → b. Real libraries (Lodash's _.set, the flat package) let you pass a different delimiter or escape dots so such keys survive. The walk is identical; only how you split the key changes.{ 'list.0.name': 'Ada' } — you'd rebuild arrays by noticing a numeric segment and creating [] instead of {} at that level, then assigning by index. Keep it opt-in; the default treats every segment as an object key so the simple round-trip stays predictable.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.