Episode 1 — Fundamentals / 1.23 — Objects

1.23.c — Accessing Properties

In one sentence: Use dot notation for clean, static access and bracket notation for dynamic or special-character keys — plus optional chaining ?. and nullish coalescing ?? to handle missing data gracefully.

Navigation: ← 1.23 Overview · 1.23.d — Adding and Deleting Properties →


1. Dot notation

The most common and readable way to access a property:

const user = {
  name: "Alice",
  age: 25,
  isAdmin: true,
};

console.log(user.name);    // "Alice"
console.log(user.age);     // 25
console.log(user.isAdmin); // true

Rules for dot notation

  • The key must be a valid JavaScript identifier (no spaces, no hyphens, does not start with a digit).
  • The key is written literally — you cannot use a variable.
const key = "name";
console.log(user.key);  // undefined — looks for a property literally called "key"
console.log(user.name); // "Alice"  — correct

2. Bracket notation

Uses a string expression inside [] to specify the key:

const user = {
  name: "Alice",
  "full name": "Alice Johnson",
  "content-type": "application/json",
};

console.log(user["name"]);         // "Alice"
console.log(user["full name"]);    // "Alice Johnson"
console.log(user["content-type"]); // "application/json"

Bracket notation is required when the key:

  • Contains spaces or special characters
  • Starts with a number
  • Is stored in a variable

3. When to use which — comparison table

FeatureDot notation obj.keyBracket notation obj["key"]
ReadabilityCleaner, preferredSlightly more verbose
Valid identifier keysYesYes
Special characters / spacesNoYes
Dynamic keys (variables)NoYes
Computed expressionsNoYes (obj["prop" + n])
Number-like keysNoYes (obj["123"])

Rule of thumb

Use dot notation by default. Switch to bracket notation when you need dynamic keys or the key is not a valid identifier.

// Dot — preferred for known, static keys
console.log(user.name);

// Bracket — required for dynamic access
const field = "name";
console.log(user[field]); // "Alice"

4. Accessing non-existent properties returns undefined

JavaScript does not throw an error when you access a property that does not exist — it silently returns undefined:

const user = { name: "Alice" };

console.log(user.age);       // undefined — no error
console.log(user.address);   // undefined — no error
console.log(user["phone"]);  // undefined — no error

This is both convenient and dangerous — you might think data exists when it does not:

const config = {};
if (config.apiUrl) {
  // This block never runs — apiUrl is undefined (falsy)
  fetch(config.apiUrl);
}

Distinguishing "missing" from "set to undefined"

const obj = { a: undefined, b: 1 };

console.log(obj.a);        // undefined
console.log(obj.c);        // undefined  — same result, different reason!

// Use `in` operator to distinguish:
console.log("a" in obj);   // true  — property exists (value is undefined)
console.log("c" in obj);   // false — property does not exist

5. Optional chaining ?.

When accessing nested properties, any intermediate null or undefined causes a TypeError:

const user = { name: "Alice" };

// This crashes:
// console.log(user.address.city); // TypeError: Cannot read properties of undefined

// Optional chaining — safe:
console.log(user.address?.city); // undefined — no error

How it works

?. short-circuits to undefined if the left side is null or undefined, instead of throwing:

const response = {
  data: {
    user: {
      name: "Alice",
      address: {
        city: "Portland",
      },
    },
  },
};

// Deep safe access
console.log(response.data?.user?.address?.city);     // "Portland"
console.log(response.data?.user?.phone?.number);     // undefined
console.log(response.data?.admin?.address?.city);    // undefined

Optional chaining with methods

const user = {
  name: "Alice",
  greet() {
    return `Hi, I'm ${this.name}`;
  },
};

console.log(user.greet?.());     // "Hi, I'm Alice"
console.log(user.goodbye?.());   // undefined — method doesn't exist, no error

Optional chaining with bracket notation

const key = "city";
const user = { address: { city: "Portland" } };

console.log(user.address?.[key]); // "Portland"
console.log(user.phone?.[key]);   // undefined

What optional chaining does NOT do

MisconceptionReality
Catches all errorsOnly handles null/undefined on the left of ?.
Makes properties existStill returns undefined — does not create missing properties
Works on non-objectsIf left side is 0, "", or false, it does not short-circuit (those are not nullish)
const obj = { count: 0 };
console.log(obj.count?.toFixed(2)); // "0.00" — 0 is not nullish, so it proceeds

6. Nullish coalescing with access — ??

The nullish coalescing operator ?? returns the right-hand side only if the left is null or undefined:

const user = {
  name: "Alice",
  bio: "",        // empty string — intentionally blank
  age: 0,         // zero — valid value
  nickname: null, // explicitly null
};

// ?? respects falsy values that are NOT null/undefined
console.log(user.bio ?? "No bio");       // "" — empty string is NOT nullish
console.log(user.age ?? 18);             // 0  — zero is NOT nullish
console.log(user.nickname ?? "No nick"); // "No nick" — null IS nullish
console.log(user.phone ?? "N/A");        // "N/A" — undefined IS nullish

// Compare with || which treats ALL falsy as "missing":
console.log(user.bio || "No bio");       // "No bio" — WRONG, overwrites ""
console.log(user.age || 18);             // 18       — WRONG, overwrites 0

Combining ?. and ??

const config = {
  database: {
    port: 5432,
  },
};

// Safe access with default
const port = config.database?.port ?? 3306;
console.log(port); // 5432

const host = config.database?.host ?? "localhost";
console.log(host); // "localhost"

const cacheSize = config.cache?.size ?? 100;
console.log(cacheSize); // 100 — config.cache is undefined

?? vs || comparison

Expression|| result?? resultWhy
0 || 10 / 0 ?? 101000 is falsy but NOT nullish
"" || "default" / "" ?? "default""default""""" is falsy but NOT nullish
null || "default" / null ?? "default""default""default"Both treat null as missing
undefined || "default" / undefined ?? "default""default""default"Both treat undefined as missing
false || true / false ?? truetruefalsefalse is falsy but NOT nullish

7. Chained access — deep property paths

Real-world data is often nested several levels deep:

const order = {
  id: 1001,
  customer: {
    name: "Alice",
    address: {
      street: "123 Main St",
      city: "Portland",
      state: "OR",
      zip: "97201",
    },
  },
  items: [
    { name: "Widget", qty: 2, price: 9.99 },
    { name: "Gadget", qty: 1, price: 24.99 },
  ],
};

// Chained dot access
console.log(order.customer.address.city); // "Portland"

// Chained access into arrays
console.log(order.items[0].name);  // "Widget"
console.log(order.items[1].price); // 24.99

// Mixed dot and bracket
const field = "city";
console.log(order.customer.address[field]); // "Portland"

Safe deep access pattern

// Without optional chaining — verbose guard
const city = order && order.customer && order.customer.address
  ? order.customer.address.city
  : "Unknown";

// With optional chaining + nullish coalescing — clean
const cityClean = order?.customer?.address?.city ?? "Unknown";

8. Bracket notation with variables — dynamic access

This is one of the most powerful patterns in JavaScript:

const user = {
  name: "Alice",
  age: 25,
  email: "alice@example.com",
};

// Access properties dynamically
const fields = ["name", "age", "email"];
for (const field of fields) {
  console.log(`${field}: ${user[field]}`);
}
// name: Alice
// age: 25
// email: alice@example.com

Dynamic form field access

function getFormValue(formData, fieldName) {
  return formData[fieldName] ?? "";
}

const formData = {
  username: "alice",
  email: "alice@example.com",
  bio: "",
};

console.log(getFormValue(formData, "username")); // "alice"
console.log(getFormValue(formData, "bio"));      // ""
console.log(getFormValue(formData, "phone"));    // ""

Building property paths dynamically

function getNestedValue(obj, path) {
  return path.split(".").reduce((current, key) => current?.[key], obj);
}

const data = {
  user: {
    address: {
      city: "Portland",
    },
  },
};

console.log(getNestedValue(data, "user.address.city")); // "Portland"
console.log(getNestedValue(data, "user.phone.number")); // undefined

9. Real examples

Accessing API response data

const apiResponse = {
  status: 200,
  data: {
    users: [
      { id: 1, name: "Alice", profile: { avatar: "alice.png" } },
      { id: 2, name: "Bob", profile: null },
    ],
  },
  meta: {
    page: 1,
    totalPages: 5,
  },
};

// Safe access patterns
const firstUser = apiResponse.data?.users?.[0];
console.log(firstUser?.name);                  // "Alice"
console.log(firstUser?.profile?.avatar);       // "alice.png"

const secondUser = apiResponse.data?.users?.[1];
console.log(secondUser?.profile?.avatar ?? "default.png"); // "default.png"

const totalPages = apiResponse.meta?.totalPages ?? 1;
console.log(totalPages); // 5

Dynamic form field rendering

const fieldConfig = {
  username: { label: "Username", required: true, maxLength: 30 },
  email: { label: "Email", required: true, type: "email" },
  bio: { label: "Bio", required: false, maxLength: 500 },
};

function renderField(fieldName) {
  const config = fieldConfig[fieldName];
  if (!config) return null;

  return {
    name: fieldName,
    label: config.label,
    required: config.required ?? false,
    maxLength: config.maxLength ?? Infinity,
    type: config.type ?? "text",
  };
}

console.log(renderField("username"));
// { name: "username", label: "Username", required: true, maxLength: 30, type: "text" }

console.log(renderField("email"));
// { name: "email", label: "Email", required: true, maxLength: Infinity, type: "email" }

Key takeaways

  1. Dot notation obj.key is clean and preferred for known, valid identifier keys.
  2. Bracket notation obj["key"] is required for dynamic keys, special characters, and variables.
  3. Accessing a non-existent property returns undefined, not an error.
  4. Optional chaining ?. short-circuits to undefined when hitting null/undefined — prevents TypeError on deep access.
  5. Nullish coalescing ?? provides defaults only for null/undefined — unlike ||, it preserves 0, "", and false.
  6. Combine ?. and ?? for safe deep access with defaults: obj?.nested?.prop ?? "default".
  7. Bracket notation with variables enables powerful dynamic property access patterns.

Explain-It Challenge

Explain without notes:

  1. When is bracket notation required instead of dot notation? Give two examples.
  2. What does optional chaining ?. do differently from regular . access? When does it short-circuit?
  3. What is the difference between || and ?? when the value is 0 or ""?

Navigation: ← 1.23 Overview · 1.23.d — Adding and Deleting Properties →