frontend / es features / 01_chaining_and_coalescing.md

Optional Chaining, Nullish Coalescing, Logical Assignment

5 min read source

Optional Chaining, Nullish Coalescing, Logical Assignment

TL;DR

Three small operators that change how you write defensive code: ?. (optional chaining — short-circuits on null/undefined), ?? (nullish coalescing — falls back on null/undefined only, not on falsy ""/0), and logical assignment (??=, ||=, &&=) for “assign if condition.” Together they replace huge swaths of if (x && x.y && x.y.z) and x.y || default boilerplate — and the ?? vs || distinction is the most common gotcha because they look interchangeable until they aren’t.

Interview Q&A

Q: Optional chaining — what does it do?

A: Short-circuits at the first null or undefined in a chain, returning undefined instead of throwing.

const user = { profile: { name: "Ada" } };
user.profile?.name;            // "Ada"
user.profile?.address?.city;   // undefined (doesn't throw)
user.profile?.address.city;    // throws — `?.` only protects the link it's on

Works for:

  • Property access: obj?.prop
  • Index access: arr?.[0]
  • Function/method calls: fn?.(), obj.method?.()

Stops the chain immediately on a nullish intermediate — no exception, no further evaluation.

Q: Common mistakes with ?..

A:

// Wrong — protects only `users`, not `users[0]`
users?.[0].name;               // throws if users[0] is undefined

// Right
users?.[0]?.name;

// Wrong — `?.` doesn't prevent the assignment crashing
user?.profile.name = "Bob";    // SyntaxError — `?.` not allowed on assignment LHS

// Right — guard separately
if (user?.profile) user.profile.name = "Bob";

Also: ?. can’t be the target of delete, ++/--, or assignment. It’s read-only.

Q: ?? vs || — when each?

A: The most asked question in this group:

| | || | ?? | |—|—|—| | Falls back on | any falsy: null, undefined, 0, "", false, NaN | only null or undefined | | When use | “non-empty default” | “default for missing value” |

const port = config.port ?? 3000;      // if not set, use 3000 — explicit port=0 is preserved
const port = config.port || 3000;      // port=0 would fall back — BUG if 0 was intentional

const name = data.name ?? "anon";      // empty string "" is preserved
const name = data.name || "anon";      // empty string falls back to "anon"

?? is safer for numeric or empty-string config. Use || only when you actually want all falsy values to fall back (rare in modern code).

Q: Logical assignment — when use?

A: Conditional assignment in one expression:

x ??= 5;           // x = x ?? 5  — assign only if x is null/undefined
x ||= 5;           // x = x || 5  — assign only if x is falsy
x &&= 5;           // x = x && 5  — assign only if x is truthy

// Common uses:
config.timeout ??= 30_000;         // set default if not provided
cache[key] ??= computeExpensive(); // memoize lazily
loaded[id] = (loaded[id] ?? 0) + 1;

Note: the RHS is only evaluated if the assignment fires. cache[key] ??= computeExpensive() doesn’t call computeExpensive() when cache[key] is already set.

Q: Combining ?. with default — what about ?.x ?? default?

A: Natural composition:

const cityName = user?.profile?.address?.city ?? "Unknown";

If any link is nullish, the chain produces undefined; ?? falls back to "Unknown". This pattern replaces:

let cityName = "Unknown";
if (user && user.profile && user.profile.address) {
  cityName = user.profile.address.city || "Unknown";
}

Five lines of defensive code become one.

Q: Type narrowing — does TS understand ?.?

A: Yes. After a ?. chain, the result type is T | undefined. Combined with ?? it narrows to T:

const cityName: string | undefined = user?.profile?.city;
const cityName: string = user?.profile?.city ?? "Unknown";

For control flow:

if (user?.profile?.city) {
  user.profile.city.toUpperCase();    // narrowed — TS knows everything is non-nullish here
}

Q: Performance — does ?. slow things down?

A: Negligible. Optional chaining compiles to runtime null checks; modern engines optimize these away in hot paths. Don’t avoid ?. for “perf.” Avoid it for “wrong semantics” — e.g., when you actually want to throw on nullish (use an assertion or explicit check).

Q: When should you not use ?.?

A:

  • When null should be an error. A ?. swallows the bug silently — user?.id returns undefined for an unauthenticated user instead of revealing the auth bug. Use explicit checks + throws/asserts at boundaries.
  • When a non-null parent invariant exists — over-using ?. reads as “I don’t trust my own types.” Trust your types; reach for ?. only when nullishness is genuinely possible.

Q: Show me the null/undefined distinction.

A: Both are nullish. ?? and ?. treat them identically. JS still has both:

  • undefined — implicit absence (declared but unassigned, missing property, missing argument).
  • null — explicit absence (deliberately “no value”).

TS lets you distinguish (x: null vs x: undefined). At runtime, the two only differ in:

  • typeof null is "object" (legacy bug); typeof undefined is "undefined".
  • JSON.stringify drops undefined properties; keeps null.

For most code, treat them as the same. The ?? operator’s whole point is unifying them.

Gotchas / edge cases

  • ?.() on a non-function throws TypeError: obj.method is not a function if obj.method exists but isn’t callable. ?. only checks for nullish.
  • Short-circuit evaluation stops the whole chain — a?.b.c() doesn’t call b.c() if a is nullish; but if a is set and b is nullish, throws on b.c. Use a?.b?.c().
  • ?? precedencea ?? b || c is a syntax error. Parens required: (a ?? b) || c. Same with &&.
  • JSON output of ??= with arrays/objects — assigns the reference, not a clone. Mutations to the assigned object reflect back.
  • Optional chaining with deletedelete a?.b doesn’t throw if a is nullish; otherwise deletes. Niche but legal.
  • TypeScript strictNullChecks interaction?. is necessary; without strict, you might write code that crashes at runtime that TS didn’t flag.

What a senior is expected to say

  • ?. short-circuits on nullish, returning undefined instead of throwing. Each link needs its own ?.a?.b.c only protects a.”
  • ?? vs ||: ?? only falls back on nullish; || on any falsy. Use ?? for numeric/empty-string defaults so 0 and '' aren’t accidentally replaced.”
  • “Logical assignment (??= etc.) for one-line conditional assigns — common for default config and lazy memoization.”
  • “Don’t ?. to hide bugs — if null should be an error, throw. Use ?. when nullishness is genuinely expected.”
  • ?. doesn’t compose with assignment (x?.y = 1 is invalid). Guard separately.”

Cross-references

Further reading