Nested Data Structures
Modelling the real world by combining arrays and objects — and the small skills (safe navigation, deep updates, JSON) that make it pleasant.
A single object or array can only get you so far. Real-world data almost always has structure inside structure: an order has line items, each line item has a product, each product has tags. Once you can navigate and update nested data confidently, you can model just about anything.
Mental model: shapes inside shapes
Take a moment to read this object slowly:
That single value packs a lot of information. The skill is being able to picture its shape — to know where things live.
Reading deep paths
To read a value from a deeply nested structure, chain dots and brackets:
order.customer.address.city // "London"
order.items[0].title // "Notebook"
order.items[order.items.length - 1] // last line itemThe hazard is missing intermediate values. If order.customer
is undefined, then order.customer.address will throw a
TypeError: Cannot read properties of undefined (reading 'address').
A classic source of beginner crashes.
Optional chaining: ?.
Modern JavaScript has a special operator for safe navigation. If
the value before ?. is null or undefined, the whole
expression returns undefined instead of throwing.
The pair of ?. and ?? (nullish coalescing) is the modern
JavaScript way to safely read possibly-missing data and fall back
to a default. Together they replace many lines of defensive
if (x && x.y && x.y.z) checks.
Walking nested data
The natural way to walk nested data is nested loops (or recursion). Let's add up the total cost of the order above:
That is just an "accumulator over a list" loop, walking one level of nesting. The same pattern scales to any depth.
A two-level example: students and grades
Outer loop walks each student. Inner loop walks that student's
grades. Two levels of nesting; two for loops. The pattern is
mechanical once you see it.
Immutable updates: deep edits without mutation
Earlier we praised the immutable update style:
const updated = { ...original, name: "new name" };That works perfectly for a one-level object. What about updating something deep inside a nested structure?
Notice we had to spread each level we wanted to copy. The remaining nested objects are shared (still by reference), which is fine — they're unchanged.
Real codebases sometimes use helper libraries (like Immer) to make deep updates pleasant. For a first pass, the explicit spread style is clearest.
Updating an item inside an array
Updating one element of an array, immutably, is a common task. Use
.map to produce a new array where one element is changed:
The unchanged elements pass through .map unmodified. The
matching element is replaced by a new object via spread.
JSON: data as text
A nested structure of objects, arrays, numbers, strings, booleans,
and null is precisely the set of things you can store as JSON
(JavaScript Object Notation). JSON is the universal exchange
format of the web — every API you'll ever call either accepts or
returns JSON.
JavaScript ships with two functions for converting between JSON text and live values:
A neat side-effect: JSON.parse(JSON.stringify(x)) is a
quick-and-dirty way to make a deep copy of plain data. It
won't handle functions, dates, or circular structures, but for
pure data it's a useful trick.
Common shapes you'll meet
It helps to recognise a few recurring shapes:
-
Array of objects (a table-like dataset)
const users = [ { id: 1, name: "Ada" }, { id: 2, name: "Linus" }, ]; -
Object of arrays (data grouped by category)
const grouped = { admins: ["Ada", "Grace"], members: ["Linus"], }; -
Map-like object (key-keyed lookups)
const usersById = { 1: { id: 1, name: "Ada" }, 2: { id: 2, name: "Linus" }, };
Almost every API response, configuration file, or database row you ever encounter is some combination of these.
A multi-file nested-data example
A small program that reads a list of orders, computes totals per customer, and prints a tidy summary. The shape of the data is non-trivial; the code is straightforward because we keep each step small.
Notice the inner nested loops (for order... for item...) — they
mirror the shape of the data. When the data has two levels of
nesting, the loop usually does too.
Challenge
Write a function getPath(obj, path, defaultValue) that:
- Takes an object
obj, a path string like"customer.address.city", and a default value. - Returns the value at that nested path, or the default if any step is missing.
- Must not throw, even if intermediate properties are missing or null.
Examples:
getPath({ a: { b: { c: 42 } } }, "a.b.c", 0)→42getPath({ a: { b: {} } }, "a.b.c", 0)→0getPath({ a: null }, "a.b.c", "n/a")→"n/a"getPath({}, "x", "n/a")→"n/a"
Why is optional chaining (a?.b?.c) so useful when reading deeply nested data?
It runs faster than ordinary property access
It automatically creates missing properties
It safely returns undefined if any link in the chain is null or undefined, instead of throwing a TypeError and crashing the program
It is a faster form of array indexing