How to flatten Object in JavaScript ?
Flattening an object means turning a nested structure into a single level of key–value pairs, where each key is the path to the original value. It's what you need when a nested object has to go somewhere flat: a form with dotted field names, a CSV export, a query string, a translation file, or a diff between two configs.
flatten({ user: { name: 'Ada', address: { city: 'London' } }, active: true });
// → { 'user.name': 'Ada', 'user.address.city': 'London', active: true }
JavaScript has no built-in for this (Array.prototype.flat() only works on arrays), so here's how to write one correctly, handle the edge cases, and reverse it.
A correct recursive version #
function flatten(obj, prefix = '', out = {}) {
for (const [key, value] of Object.entries(obj)) {
const path = prefix ? `${prefix}.${key}` : key;
if (isPlainObject(value) && Object.keys(value).length > 0) {
flatten(value, path, out); // go deeper, carrying the path
} else {
out[path] = value; // leaf: store under the full path
}
}
return out;
}
function isPlainObject(value) {
return value !== null && typeof value === 'object' &&
Object.getPrototypeOf(value) === Object.prototype;
}
The key detail is carrying the path down the recursion. A common mistake is to spread each nested result into the output without a prefix. That loses the path, so { a: { x: 1 }, b: { x: 2 } } collapses into a single x and one value silently overwrites the other.
Handling arrays #
Arrays can be treated as leaves (kept whole) or flattened by index. Flattening by index is usually what exports and forms want:
function flatten(obj, prefix = '', out = {}) {
for (const [key, value] of Object.entries(obj)) {
const path = prefix ? `${prefix}.${key}` : key;
const nested = Array.isArray(value) || isPlainObject(value);
if (nested && Object.keys(value).length > 0) {
flatten(value, path, out);
} else {
out[path] = value;
}
}
return out;
}
flatten({ tags: ['js', 'ts'], team: [{ name: 'Ada' }] });
// → { 'tags.0': 'js', 'tags.1': 'ts', 'team.0.name': 'Ada' }
Object.entries() works on arrays too, returning the indexes as keys, so the same loop handles both.
Edge cases worth deciding up front #
| Value | What the function above does | Why |
|---|---|---|
null |
Kept as a leaf | typeof null === 'object', so it must be excluded explicitly |
{} or [] (empty) |
Kept as a leaf | Otherwise the key disappears entirely |
Date, Map, class instances |
Kept as leaves | isPlainObject only recurses into plain objects |
Keys containing . |
Ambiguous | { 'a.b': 1 } and { a: { b: 1 } } flatten to the same key; use a different separator if your keys contain dots |
| Circular references | Infinite recursion | Track visited objects in a WeakSet if your input can be cyclic |
A configurable separator solves the dotted-key problem:
const flattenWith = (sep) => function flat(obj, prefix = '', out = {}) {
for (const [key, value] of Object.entries(obj)) {
const path = prefix ? `${prefix}${sep}${key}` : key;
if (isPlainObject(value) && Object.keys(value).length) flat(value, path, out);
else out[path] = value;
}
return out;
};
flattenWith('/')({ a: { b: 1 } }); // → { 'a/b': 1 }
Unflattening: going back #
The reverse walks each path and creates objects along the way:
function unflatten(flat, sep = '.') {
const out = {};
for (const [path, value] of Object.entries(flat)) {
const keys = path.split(sep);
let node = out;
keys.forEach((key, i) => {
if (i === keys.length - 1) node[key] = value;
else node = node[key] ??= /^\d+$/.test(keys[i + 1]) ? [] : {};
});
}
return out;
}
unflatten({ 'user.name': 'Ada', 'tags.0': 'js', 'tags.1': 'ts' });
// → { user: { name: 'Ada' }, tags: ['js', 'ts'] }
A numeric next key creates an array, so array paths round-trip correctly.
A typed version for TypeScript #
type Flat = Record<string, unknown>;
export function flatten(obj: Record<string, unknown>, prefix = '', out: Flat = {}): Flat {
for (const [key, value] of Object.entries(obj)) {
const path = prefix ? `${prefix}.${key}` : key;
if (value !== null && typeof value === 'object' && !(value instanceof Date) &&
Object.keys(value).length > 0) {
flatten(value as Record<string, unknown>, path, out);
} else {
out[path] = value;
}
}
return out;
}
When to use a library instead #
For one-off needs, the 15 lines above are enough. If you need many options (maximum depth, safe handling of cycles, custom leaf rules), the small flat package on npm is a well-tested choice. For deep copies rather than flattening, see structuredClone() in JavaScript, and for flattening arrays of arrays, see how to flatten arrays in JavaScript.
Up next in JavaScript
More from this topic
Enjoyed this article?
Get new JavaScript tutorials delivered. No spam — just code-first articles when they ship.
3 comments



Pingback: Currying in JavaScript: How It Works, With Examples
Pingback: JavaScript Type Coercion and Equality: Why `[] == ![]` is `true` - Code-JS
Pingback: JavaScript WeakMap, WeakSet, and WeakRef: When Regular Maps Hold On Forever - Code-JS