Course outline · 0% complete

0/26 lessons0%

Course overview →

null, undefined, and Optional Access

lesson 4-4 · ~11 min · 14/26

The missing-value unions

The most common JavaScript crash is reading a property of a value that is not there: Cannot read properties of undefined. Every real program has values that can be missing, such as a search with no match, an optional field never filled in, or a setting the user skipped. TypeScript's answer is to put the absence into the type, then apply this unit's narrowing discipline to it.

JavaScript has two absence values: undefined (what you get when nothing was assigned, such as a missing property) and null (a value code assigns on purpose to mean "empty"). TypeScript treats each as its own type, so a function that can fail says so in its signature:

function firstStartingWith(names: string[], letter: string): string | null {
  for (const n of names) {
    if (n.startsWith(letter)) {
      return n;
    }
  }
  return null;
}

The return type is honest: usually a string, sometimes null. Callers cannot call string methods on the result until a check such as !== null narrows the null away, which is the union rule from lesson 4-1 applied to absence.

This one convention eliminates the whole class of "forgot the no-match case" bugs, because forgetting is now a compile error rather than a crash for one unlucky user.

Narrowing away a possible null

function firstStartingWith(names: string[], letter: string): string | null {
  for (const n of names) {
    if (n.startsWith(letter)) {
      return n;
    }
  }
  return null;
}

const names = ["ada", "grace", "alan"];
const hit = firstStartingWith(names, "g");
if (hit !== null) {
  console.log("found " + hit);
} else {
  console.log("no match");
}
console.log(firstStartingWith(names, "z"));

Output

found grace
null

The first caller narrows before use. The last line prints the raw result instead, so a missed search prints null.

Details worth tracking

  • Inside if (hit !== null), hit is a plain string, so string methods compile there and only there. Calling .toUpperCase() on that final unchecked result is blocked until you narrow it.
  • startsWith is an ordinary string method, so "grace".startsWith("g") is true. Nothing about the union changes how the loop body works.
  • Concatenating hit into "found " + hit works in the narrowed branch. The else branch cannot use it as a string at all, which is the point.

?. and ??: the built-in shortcuts

Narrowing with if works everywhere but gets wordy when absence is routine. JavaScript ships two operators for exactly this, and TypeScript understands both fully.

Optional chaining a?.b reads the property only when a is not null or undefined. Otherwise the whole expression evaluates to undefined instead of crashing. The chain stops at the first missing link, so p.team?.city has type string | undefined.

Nullish coalescing x ?? fallback produces x unless it is null or undefined, in which case it produces the fallback. It exists because the older || trick also replaces perfectly valid values. 0, "", and false are all "falsy", so count || 1 silently turns a legitimate 0 into 1. ?? replaces only true absence.

const city = p.team?.city ?? "no city";

Read it as: take the team's city if there is a team, otherwise use "no city". The result is a plain string with no union left, because the fallback closed the undefined hole. The pair ?. with ?? is the everyday idiom for optional data in production TypeScript.

Optional chaining and a fallback in one line

Player has an optional team, and describe prints a city whether or not the team is there.

interface Player {
  name: string;
  team?: { city: string; title: string };
}

function describe(p: Player): string {
  const city = p.team?.city ?? "no city";
  return p.name + " (" + city + ")";
}

console.log(describe({ name: "Mia", team: { city: "Austin", title: "Owls" } }));
console.log(describe({ name: "Sam" }));

Output

Mia (Austin)
Sam (no city)

Sam has no team, so p.team?.city evaluates to undefined and ?? supplies the fallback.

Why each operator is required here

  • Without ?., the expression p.team.city would be a compile error, because team is possibly undefined.
  • Without ??, the type of city would stay string | undefined, and concatenating it would print the word undefined for Sam.
  • Replacing ?? with || looks equivalent until a city is legitimately the empty string. || would swallow it and print "no city", while ?? keeps it, because "" is a real value.

?? compared with ||

?? uses the fallback only when the value is null or undefined. || also replaces "falsy" values such as 0 and the empty string.

|| falls back whenever the left side is falsy, which wrongly swallows legitimate values including 0, "", and false. A line like const pageSize = input || 20 quietly rewrites a deliberate 0 into 20.

?? asks one precise question, whether the value is null or undefined, so real values always survive. Prefer ?? whenever a type includes null or undefined.

Expressionvalue is 0value is ""value is null
value ?? "fb"0"""fb"
value || "fb""fb""fb""fb"

noteLength: absence closed in a single expression

noteLength(o: Order): number returns the length of an order's note, or 0 when there is no note, in one line using ?. and ??.

interface Order {
  id: number;
  note?: string;
}

function noteLength(o: Order): number {
  return o.note?.length ?? 0;
}

console.log(noteLength({ id: 1, note: "gift wrap" }));
console.log(noteLength({ id: 2 }));

Output

9
0

Reading the one-liner

  • o.note?.length has type number | undefined, and the ?? 0 turns it into a plain number, which is what the return type promises.
  • "gift wrap" is 9 characters, counting the space.
  • The second call has no note at all, so the chain short-circuits to undefined before .length is ever reached, and the fallback takes over.
  • The whole guard fits in one expression, which is why this idiom shows up constantly in real code instead of a four-line if.