Unions with null and Narrowing

Optional Values

The most common union is T | null or T | undefined. TypeScript narrows the type after a check, so inside the if block it is no longer nullable.

Example

type User = { name: string; coupon: string | null };

function checkoutMessage(user: User): string {
  if (user.coupon === null) {
    return user.name + ", no coupon applied";
  }
  return user.name + ", coupon " + user.coupon.toUpperCase() + " applied";
}

console.log(checkoutMessage({ name: "Asha", coupon: null }));
console.log(checkoutMessage({ name: "Ravi", coupon: "save10" }));

Output

Asha, no coupon applied
Ravi, coupon SAVE10 applied

Literal Unions as Tags

Example

When each object in a union has a common literal field like kind, a switch on that field narrows perfectly. This pattern (a discriminated union) is covered in depth in the medium level.

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rect"; width: number; height: number };

function area(s: Shape): number {
  switch (s.kind) {
    case "circle": return Math.PI * s.radius ** 2;
    case "rect": return s.width * s.height;
  }
}

console.log(area({ kind: "circle", radius: 1 }).toFixed(2));
console.log(area({ kind: "rect", width: 3, height: 4 }));

Output

3.14
12

Practice

Exercise: Write a type Theme = "light" | "dark" | "system" and a function nextTheme(t: Theme): Theme that cycles light -> dark -> system -> light.

Show answer
type Theme = "light" | "dark" | "system";

function nextTheme(t: Theme): Theme {
  if (t === "light") return "dark";
  if (t === "dark") return "system";
  return "light";
}

console.log(nextTheme("light"), nextTheme("system"));