Structuring TypesIntermediate8 min05 / 7

Unions & Narrowing

Learn how to express "one of several types" with union types, and how TypeScript's narrowing guards let you work safely with each possibility.

Real-world values rarely fit neatly into a single type. A form field might hold a string or be null when the user hasn't filled it in yet. A button might accept a size of "sm", "md", or "lg" — nothing else. TypeScript handles both situations with union types: the | operator that means "this OR that". Once you know how to define a union, you'll also learn narrowing — TypeScript's way of zooming in on the specific type inside an if branch so you get full type safety, not just a best-guess.

#Your First Union Type

Think of it like

Think of a USB-C port

A USB-C port accepts power cables, data cables, and video cables — it doesn't care which one you plug in, as long as it's a valid USB-C connector. A union type is the same idea: write | between the types you want to allow, and TypeScript rejects everything else.

Use | to combine types. TypeScript enforces that only the listed types are accepted.
// A variable that can hold a string OR a number
let id: string | number;

id = "user-42";   // ✅ fine
id = 99;          // ✅ also fine
// id = true;     // ❌ boolean is not in the union

#Literal Unions — Locking Down Exact Values

Literal unions are a hand-picked whitelist. TypeScript catches typos at compile time and your editor autocompletes the valid options.
type Size = "sm" | "md" | "lg";
type StatusCode = 200 | 404 | 500;

function resize(size: Size): void {
  console.log(`Resizing to: ${size}`);
}

resize("md");    // ✅ valid
// resize("xl"); // ❌ "xl" is not assignable to type Size

#Narrowing with typeof

typeof is a narrowing guard: inside the if block TypeScript narrows the union to just the matched type, unlocking type-specific methods.
function formatId(id: string | number): string {
  if (typeof id === "string") {
    // TypeScript KNOWS id is a string inside this block
    return id.toUpperCase();
  }
  // Down here, TypeScript KNOWS id is a number
  return id.toFixed(0);
}

console.log(formatId("user-42")); // USER-42
console.log(formatId(7.9));       // 8

#Narrowing with in — Checking Object Shape

The in operator checks whether a property key exists, letting TypeScript narrow between object shapes.
type Cat = { meow: () => void };
type Dog = { bark: () => void };

function makeNoise(animal: Cat | Dog): void {
  if ("meow" in animal) {
    animal.meow(); // TypeScript knows: Cat
  } else {
    animal.bark(); // TypeScript knows: Dog
  }
}

#Handling null and undefined

A strict equality check against null is all TypeScript needs to narrow away the null possibility.
function greet(name: string | null): string {
  if (name === null) {
    return "Hello, stranger!";
  }
  return `Hello, ${name}!`; // TypeScript knows name is a string here
}

console.log(greet("Alice")); // Hello, Alice!
console.log(greet(null));    // Hello, stranger!
Common mistake

Truthiness checks silently swallow 0 and ""

It's tempting to write if (value) to guard against null, but falsy values also include 0, "" (empty string), and false. Those are often valid values you don't want to skip.

Use === null or !== undefined for precise null checks:

```ts function double(n: number | null): number { // BAD — skips when n is 0! // if (n) { return n * 2; }

// GOOD — only guards against null if (n !== null) { return n * 2; } return 0; } ```

Quick check

You have a variable typed as string | number | null. You write: if (typeof value === "string") { ... }. Inside that block, what does TypeScript think the type of value is?

Key takeaways

  • The | operator creates a union type that accepts any of the listed types or literal values.
  • Literal unions like "sm" | "md" | "lg" act as a compile-time whitelist, catching typos and enabling autocomplete.
  • typeof, in, and equality checks narrow a union to a specific type inside a branch, unlocking type-safe operations.
  • Avoid bare truthiness checks when 0, "", or false are valid values — prefer explicit null or undefined comparisons.
  • Union types are how TypeScript models the messy, optional, multi-shape data that real applications deal with every day.
Practice challenges
Test yourself · earn XP
0/4
Predict the output#1

Types are erased at runtime, but the narrowing logic still runs. What does this snippet print?

predict-output
function formatId(id: string | number): string {
  if (typeof id === "string") {
    return id.toUpperCase();
  }
  return id.toFixed(0);
}

console.log(formatId("user-42"));
console.log(formatId(7.9));
Fix the bug#2

This code has a bug — what's wrong?

fix-bug
function double(n: number | null): number {
  if (n) {
    return n * 2;
  }
  return 0;
}

console.log(double(0));
Fill in the blank#3

Fill in the narrowing guards. Use typeof to detect the string, and the in operator to detect a Cat by its 'meow' property.

function describe(x: string | number): string {
  if (typeof x === ) {
    return x.toUpperCase();
  }
  return x.toFixed(0);
}

type Cat = { meow: () => void };
type Dog = { bark: () => void };
function speak(animal: Cat | Dog): void {
  if ( in animal) {
    animal.meow();
  } else {
    animal.bark();
  }
}
Reorder the lines#4

Arrange these lines into a function that handles string | null: return a greeting for a real name, but 'Hello, stranger!' when the value is null. Narrow away null first.

1
  return `Hello, ${name}!`;
2
  }
3
  if (name === null) {
4
    return "Hello, stranger!";
5
}
6
function greet(name: string | null): string {
Your turn
Practice exercise

Write a TypeScript function called describe that accepts a parameter value typed as string | number | boolean | null. It should return a string: - If value is null → return "nothing here" - If value is a boolean → return "flag: true" or "flag: false" - If value is a number → return "number: 42" (with the actual number) - If value is a string → return "text: hello" (with the actual string)

Use typeof and a null check to narrow the type in each branch.

Try it live — edit the code and hit Run to see the output:

solution.ts · editable