Negative Array Indexing
advancedUse a Proxy to support arr[-1] like Python — a practical introduction to Proxy traps.
Flatten nested objects into dot-path keys in JavaScript ({ 'a.b.c': 1 }), unflatten them back, and handle arrays, empty objects, cycles, and the delimiter-collision trap.
Implement
flatten(obj): nested objects become one flat object with dot-separated key paths — and its inverse,unflatten.
flatten({ user: { name: "Ada", address: { city: "London" } }, active: true });
// { "user.name": "Ada", "user.address.city": "London", "active": true }A top-tier frequency question (it's a Stripe classic — their API errors and metadata use exactly this shape) with a satisfying structure: the recursion is deep-flatten-for-objects, but the design decisions — array notation, empty containers, delimiter collisions, and whether the inverse can even be lossless — are where the interview lives.
Form libraries (Formik/react-hook-form errors: "items.2.price": "required"), Firestore/Mongo dot-path updates (update({ "profile.city": "Paris" }) — path-writes avoid clobbering siblings), analytics event properties (flat key-value schemas), CSV/spreadsheet export, i18n key files, and diff/patch tooling. The unifying reason: flat keys make nested data addressable — one string names one leaf.
function flatten(obj, { delimiter = ".", keepEmpty = true } = {}) {
const result = {};
function walk(value, path) {
const isObject =
value !== null && typeof value === "object" && !(value instanceof Date);
if (!isObject) {
result[path] = value; // leaf: primitive, null, Date, function…
return;
}
const keys = Array.isArray(value)
? value.map((_, i) => i)
: Object.keys(value);
if (keys.length === 0) {
// {} and [] have no leaves — keep them or lose them? A POLICY.
if (keepEmpty && path) result[path] = Array.isArray(value) ? [] : {};
return;
}
for (const key of keys) {
walk(value[key], path ? `${path}${delimiter}${key}` : String(key));
}
}
walk(obj, "");
return result;
}flatten({ a: { b: 1, c: [10, 20] }, d: null });
// { "a.b": 1, "a.c.0": 10, "a.c.1": 20, "d": null }
flatten({ a: { b: {} } }); // { "a.b": {} } — empties preserved
flatten({ a: { b: {} } }, { keepEmpty: false }); // {} — or dropped, your callDecisions to narrate as you code:
null (careful: typeof null === "object" — the classic), primitives, and — deliberately — Dates and other class instances: recursing into a Date's internals is never what a caller wants. A isPlainObjectOrArray predicate is the honest boundary; naming that the boundary is a choice is the senior move.a.c.0) or bracket notation (a.c[0])? Dot-indices round-trip more simply; brackets read better to humans and match lodash's get/set. Pick one, say why, mention the other.{}/[] — with no leaves they vanish silently unless you keep them explicitly, and then unflatten can't restore them: the round trip is lossy without the keepEmpty marker. Interviewers plant { a: {} } precisely for this.function unflatten(flat, { delimiter = "." } = {}) {
const result = {};
for (const [path, value] of Object.entries(flat)) {
const segments = path.split(delimiter);
let node = result;
for (let i = 0; i < segments.length - 1; i++) {
const seg = segments[i];
if (seg === "__proto__" || seg === "constructor" || seg === "prototype") {
continue; // ← prototype-pollution guard; see below
}
if (node[seg] === null || typeof node[seg] !== "object") {
// create the container; look AHEAD to decide array vs object
node[seg] = /^\d+$/.test(segments[i + 1]) ? [] : {};
}
node = node[seg];
}
node[segments.at(-1)] = value;
}
return result;
}unflatten({ "a.b": 1, "a.c.0": 10, "a.c.1": 20 });
// { a: { b: 1, c: [10, 20] } } — the look-ahead rebuilt the arrayThe security paragraph that upgrades this answer: unflatten on untrusted input is a textbook prototype pollution vector — unflatten({ "__proto__.isAdmin": true }) without the guard writes onto Object.prototype, and every object in the process suddenly has isAdmin. This exact bug has CVEs in lodash (_.set), jQuery, and dozens of packages. The guard above (skip __proto__/constructor/prototype segments) or building with null-prototype objects (the Object.groupBy trick) closes it. Mentioning pollution unprompted, with the lodash CVE as the citation, is the strongest single sentence available in this question.
{ "a.b": 1 } (a literal dot in one key) flattens identically to { a: { b: 1 } }: the round trip is ambiguous. Fixes: escape delimiters, choose an unlikely delimiter, or store paths as arrays internally (the lossless representation — it's what lodash's get/set accept). Knowing flat-string paths are fundamentally lossy is the theory point.keepEmpty policy above.{ a: { 0: "x" } } and { a: ["x"] } flatten identically; unflatten's digit-heuristic will pick the array. Lossy again — same root cause, worth grouping with the delimiter issue as "flattening erases type distinctions."a.self = a recurses forever; the ancestor-set guard from deep clone applies if inputs aren't trusted to be trees.Object.keys skips them)… but our value.map((_, i) => i) visits them as undefined leaves. Two defensible behaviors; the sparse-array weirdness strikes again.unflatten (the security miss).null as a container (typeof null === "object") and crashing on Object.keys(null).Dates/class instances and emitting garbage paths.+ concatenation including a leading delimiter (.a.b) — the path ? ... : key seed handles the root.get(obj, "a.b.0") and set using these ideas." — the same segment-walk without the full transform; set shares unflatten's container-creation logic (and its pollution guard!) — this is literally lodash _.get/_.set.depth parameter, stop recursing and emit the subtree as the leaf — mirrors flat(depth)."user.address.city" unions from the object type — at the edge of practical, and naming Paths<T>-style types (as in react-hook-form's typings) shows you've seen it done for real.Use a Proxy to support arr[-1] like Python — a practical introduction to Proxy traps.
Flatten arbitrarily nested arrays recursively and iteratively — then compare with Array.prototype.flat.
Structural equality for nested data: type checks, key comparison, and recursion done right.
Recursively clone nested objects and arrays, handling cycles with a WeakMap.