Episode 1 — Fundamentals / 1.23 — Objects

1.23.e — Nested Objects

In one sentence: Real-world data is almost always nested — objects inside objects — and working safely with deep structures requires optional chaining, understanding shallow vs deep copy, and knowing how to update nested data without unintended mutation.

Navigation: ← 1.23 Overview · 1.23.f — Looping Through Keys →


1. Objects inside objects — deep data structures

Most real-world data is not flat. A user has an address; an address has a city and coordinates; coordinates have latitude and longitude:

const user = {
  id: 1,
  name: "Alice",
  address: {
    street: "123 Main St",
    city: "Portland",
    state: "OR",
    zip: "97201",
    coordinates: {
      lat: 45.5152,
      lng: -122.6784,
    },
  },
  preferences: {
    theme: "dark",
    notifications: {
      email: true,
      sms: false,
      push: true,
    },
  },
  tags: ["admin", "beta-tester"],
};

This is three levels deep in some paths (user.preferences.notifications.email). API responses, configuration files, and database documents regularly have this kind of nesting.


2. Accessing nested properties

Use chained dot or bracket notation:

// Direct chained access
console.log(user.address.city);                    // "Portland"
console.log(user.address.coordinates.lat);         // 45.5152
console.log(user.preferences.notifications.email); // true

// Mixed with bracket notation (for dynamic keys)
const field = "city";
console.log(user.address[field]);                  // "Portland"

// Accessing arrays inside objects
console.log(user.tags[0]);                         // "admin"
console.log(user.tags.length);                     // 2

3. The danger — TypeError if intermediate is null/undefined

If any intermediate value in the chain is null or undefined, the next . access throws:

const user = {
  name: "Alice",
  // address is missing entirely
};

// CRASH:
// console.log(user.address.city);
// TypeError: Cannot read properties of undefined (reading 'city')

// Also crashes if a value is explicitly null:
const user2 = {
  name: "Bob",
  address: null,
};
// console.log(user2.address.city);
// TypeError: Cannot read properties of null (reading 'city')

This is one of the most common runtime errors in JavaScript.


4. Optional chaining for safe deep access

The ?. operator short-circuits to undefined instead of throwing:

const user = { name: "Alice" };

// Safe — returns undefined instead of crashing
console.log(user.address?.city);                       // undefined
console.log(user.address?.coordinates?.lat);           // undefined
console.log(user.preferences?.notifications?.email);   // undefined

Combining with nullish coalescing for defaults

const user = {
  name: "Alice",
  preferences: {
    theme: "dark",
    // notifications not set
  },
};

const emailNotif = user.preferences?.notifications?.email ?? true;
console.log(emailNotif); // true (default)

const theme = user.preferences?.theme ?? "light";
console.log(theme); // "dark" (actual value)

Safe method calls on nested objects

const app = {
  logger: {
    info(msg) { console.log(`[INFO] ${msg}`); },
  },
};

app.logger?.info("Application started");    // [INFO] Application started
app.analytics?.track("page_view");          // undefined — no crash

5. Deep copying vs shallow copying — the problem

When you copy an object, you need to understand what "copy" means:

const original = {
  name: "Alice",
  scores: [95, 88, 72],
  address: {
    city: "Portland",
  },
};

// "Copying" the reference — NOT a copy at all
const ref = original;
ref.name = "Bob";
console.log(original.name); // "Bob" — same object!

The real question is: when you create a new object, do nested objects get their own copies or do they share references?


6. Shallow copy — { ...obj } and Object.assign({}, obj)

A shallow copy creates a new top-level object, but nested objects are still shared:

const original = {
  name: "Alice",
  scores: [95, 88, 72],
  address: {
    city: "Portland",
    state: "OR",
  },
};

// Shallow copy with spread
const copy = { ...original };

// Top-level properties are independent
copy.name = "Bob";
console.log(original.name); // "Alice" — not affected

// BUT nested objects are SHARED
copy.address.city = "Seattle";
console.log(original.address.city); // "Seattle" — CHANGED! Both point to same address object

copy.scores.push(100);
console.log(original.scores); // [95, 88, 72, 100] — CHANGED! Both point to same array

Visual explanation

original ──→ { name: "Alice", address: ──→ { city: "Portland" } }
                                     ↑
copy     ──→ { name: "Bob",   address: ─┘   (same reference!)

Object.assign behaves the same way

const copy2 = Object.assign({}, original);
// Same shallow behavior — nested objects are still shared

7. Deep copy — structuredClone(obj) (modern)

structuredClone creates a completely independent copy at all levels:

const original = {
  name: "Alice",
  scores: [95, 88, 72],
  address: {
    city: "Portland",
    coordinates: { lat: 45.52, lng: -122.68 },
  },
  createdAt: new Date("2024-01-15"),
};

const deepCopy = structuredClone(original);

// Modifying the deep copy does NOT affect the original
deepCopy.address.city = "Seattle";
deepCopy.scores.push(100);
deepCopy.address.coordinates.lat = 47.61;

console.log(original.address.city);            // "Portland" — safe!
console.log(original.scores);                  // [95, 88, 72] — safe!
console.log(original.address.coordinates.lat); // 45.52 — safe!

What structuredClone handles

TypeSupported?
Plain objectsYes
ArraysYes
DateYes
Map, SetYes
RegExpYes
ArrayBuffer, typed arraysYes
FunctionsNo — throws DataCloneError
DOM nodesNo
Symbols (as property keys)No
Prototype chainNot preserved (always plain object)
// structuredClone cannot handle functions
const withMethod = {
  name: "Alice",
  greet() { return "hi"; },
};
// structuredClone(withMethod); // DataCloneError!

8. Legacy deep copy — JSON.parse(JSON.stringify(obj))

Before structuredClone, the JSON round-trip was the common workaround:

const original = {
  name: "Alice",
  address: { city: "Portland" },
  scores: [95, 88],
};

const deepCopy = JSON.parse(JSON.stringify(original));

deepCopy.address.city = "Seattle";
console.log(original.address.city); // "Portland" — deep copy works

Limitations of JSON round-trip

Value typeWhat happens
undefinedRemoved from object; becomes null in array
DateConverted to string (not restored as Date)
RegExpBecomes {}
Map, SetBecomes {}
Infinity, NaNBecome null
FunctionsRemoved
Circular referencesThrows TypeError
const problematic = {
  date: new Date("2024-01-15"),
  pattern: /hello/gi,
  value: undefined,
  count: NaN,
};

const copy = JSON.parse(JSON.stringify(problematic));
console.log(copy);
// { date: "2024-01-15T00:00:00.000Z", pattern: {}, count: null }
// date is now a string, pattern is empty object, value is gone, NaN is null

Comparison: structuredClone vs JSON round-trip

FeaturestructuredCloneJSON.parse(JSON.stringify)
DatePreserved as DateBecomes string
undefinedPreservedRemoved / null
Map / SetPreservedLost (becomes {})
NaN / InfinityPreservedBecomes null
FunctionsThrowsSilently removed
Circular referencesHandles themThrows
PerformanceFaster (native)Slower (serialize + parse)
Browser supportModern (2022+)Universal

Recommendation: Use structuredClone unless you need to support very old environments.


9. Modifying nested properties without mutation

In React, Redux, and functional programming, you often need to update a nested value while keeping the original intact. This requires spreading at each level:

const state = {
  user: {
    name: "Alice",
    address: {
      city: "Portland",
      state: "OR",
    },
  },
  settings: {
    theme: "dark",
  },
};

// Update city without mutating state
const newState = {
  ...state,                      // copy top level
  user: {
    ...state.user,               // copy user level
    address: {
      ...state.user.address,     // copy address level
      city: "Seattle",           // override city
    },
  },
};

console.log(state.user.address.city);    // "Portland" — unchanged
console.log(newState.user.address.city); // "Seattle"  — updated

// Settings is shared (not spread), which is fine if we didn't change it
console.log(state.settings === newState.settings); // true — same reference (efficient)

The spread-at-every-level pattern

To update: state.user.address.city

{ ...state,
  user: { ...state.user,
    address: { ...state.user.address,
      city: "new value"
    }
  }
}

Each level creates a new object, but sibling properties at each level keep their references — this is structural sharing and is memory-efficient.

Helper function for nested updates

function updateNested(obj, path, value) {
  const keys = path.split(".");
  if (keys.length === 1) {
    return { ...obj, [keys[0]]: value };
  }
  const [first, ...rest] = keys;
  return {
    ...obj,
    [first]: updateNested(obj[first] ?? {}, rest.join("."), value),
  };
}

const original = {
  user: { profile: { name: "Alice", age: 25 } },
};

const updated = updateNested(original, "user.profile.name", "Bob");
console.log(updated.user.profile.name);  // "Bob"
console.log(original.user.profile.name); // "Alice" — unchanged

10. Real examples

User with address and preferences

const user = {
  id: 42,
  name: "Alice",
  email: "alice@example.com",
  address: {
    street: "123 Main St",
    city: "Portland",
    state: "OR",
    country: "US",
  },
  preferences: {
    theme: "dark",
    language: "en",
    notifications: {
      email: true,
      sms: false,
      push: true,
      frequency: "daily",
    },
  },
};

// Safe read
const smsEnabled = user.preferences?.notifications?.sms ?? false;
console.log(smsEnabled); // false

// Immutable update — change notification frequency
const updatedUser = {
  ...user,
  preferences: {
    ...user.preferences,
    notifications: {
      ...user.preferences.notifications,
      frequency: "weekly",
    },
  },
};

console.log(updatedUser.preferences.notifications.frequency); // "weekly"
console.log(user.preferences.notifications.frequency);         // "daily"

API response with nested data

const apiResponse = {
  status: 200,
  data: {
    article: {
      id: 101,
      title: "Understanding Objects",
      author: {
        id: 5,
        name: "Alice",
        avatar: "alice.png",
      },
      comments: [
        {
          id: 201,
          text: "Great article!",
          author: { id: 8, name: "Bob" },
          replies: [
            { id: 301, text: "Thanks!", author: { id: 5, name: "Alice" } },
          ],
        },
      ],
      metadata: {
        views: 1500,
        likes: 42,
        tags: ["javascript", "objects", "tutorial"],
      },
    },
  },
};

// Safe deep access
const authorName = apiResponse.data?.article?.author?.name ?? "Unknown";
console.log(authorName); // "Alice"

const firstReply = apiResponse.data?.article?.comments?.[0]?.replies?.[0];
console.log(firstReply?.text); // "Thanks!"

const tags = apiResponse.data?.article?.metadata?.tags ?? [];
console.log(tags.join(", ")); // "javascript, objects, tutorial"

// Deep copy for safe manipulation
const responseCopy = structuredClone(apiResponse);
responseCopy.data.article.metadata.views += 1;
console.log(apiResponse.data.article.metadata.views); // 1500 — original untouched
console.log(responseCopy.data.article.metadata.views); // 1501

Key takeaways

  1. Nested objects are the norm in real-world data — API responses, configs, state trees.
  2. Accessing a property on null or undefined throws TypeError — use optional chaining ?. to prevent crashes.
  3. Shallow copy ({ ...obj }, Object.assign) only copies the top level; nested objects are shared references.
  4. structuredClone(obj) creates a true deep copy — independent at every level. Use it as the modern default.
  5. JSON.parse(JSON.stringify(obj)) is a legacy deep copy that loses Date, undefined, Map, Set, NaN, and cannot handle circular references.
  6. For immutable nested updates, spread at every level of the path you are changing — sibling references are efficiently shared.
  7. Consider helper functions or libraries (Immer, Lodash _.set) for deeply nested immutable updates to avoid boilerplate.

Explain-It Challenge

Explain without notes:

  1. Why does modifying a shallow copy's nested property also change the original?
  2. What is the difference between structuredClone and the JSON.parse(JSON.stringify) trick? Name two values they handle differently.
  3. Show the spread-at-every-level pattern to update state.user.address.city immutably.

Navigation: ← 1.23 Overview · 1.23.f — Looping Through Keys →