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
| Feature | Dot notation obj.key | Bracket notation obj["key"] |
|---|---|---|
| Readability | Cleaner, preferred | Slightly more verbose |
| Valid identifier keys | Yes | Yes |
| Special characters / spaces | No | Yes |
| Dynamic keys (variables) | No | Yes |
| Computed expressions | No | Yes (obj["prop" + n]) |
| Number-like keys | No | Yes (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
| Misconception | Reality |
|---|---|
| Catches all errors | Only handles null/undefined on the left of ?. |
| Makes properties exist | Still returns undefined — does not create missing properties |
| Works on non-objects | If 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 | ?? result | Why |
|---|---|---|---|
0 || 10 / 0 ?? 10 | 10 | 0 | 0 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 ?? true | true | false | false 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
- Dot notation
obj.keyis clean and preferred for known, valid identifier keys. - Bracket notation
obj["key"]is required for dynamic keys, special characters, and variables. - Accessing a non-existent property returns
undefined, not an error. - Optional chaining
?.short-circuits toundefinedwhen hittingnull/undefined— preventsTypeErroron deep access. - Nullish coalescing
??provides defaults only fornull/undefined— unlike||, it preserves0,"", andfalse. - Combine
?.and??for safe deep access with defaults:obj?.nested?.prop ?? "default". - Bracket notation with variables enables powerful dynamic property access patterns.
Explain-It Challenge
Explain without notes:
- When is bracket notation required instead of dot notation? Give two examples.
- What does optional chaining
?.do differently from regular.access? When does it short-circuit? - What is the difference between
||and??when the value is0or""?
Navigation: ← 1.23 Overview · 1.23.d — Adding and Deleting Properties →