Lesson 7 of 49 beginner

Union Types & Type Narrowing

Being Flexible While Safe

Open interactive version (quiz + challenge)

Real-world analogy

A union type is like a parking spot that accepts 'cars OR motorcycles.' Type narrowing is like checking WHICH vehicle is parked there before trying to ride it. You narrow down the possibilities until you KNOW what type it is!

What is it?

Union types let variables be multiple types. Type narrowing means checking 'what type is it RIGHT NOW?' in different code branches. After you narrow, TypeScript knows the specific type and prevents type errors.

Real-world relevance

Union types power real-world features: API responses (success | error), user states (loggedIn | loggedOut), form status (idle | loading | error | success).

Key points

Code example

// Union type — string OR number
let id: string | number;
id = "abc";    // OK
id = 123;      // OK
// id = true;  // ERROR!

// Literal union — only specific values
type Status = "active" | "inactive" | "banned";
let status: Status = "active";     // OK
// status = "deleted";             // ERROR!

// Using unions in functions
function printId(id: string | number) {
  if (typeof id === "string") {
    console.log(id.toUpperCase());  // TS knows: string
  } else {
    console.log(id.toFixed(2));     // TS knows: number
  }
}

// Truthiness narrowing
function greet(name: string | null) {
  if (name) {
    // name is string (not null)
    console.log("Hello " + name);
  } else {
    console.log("Hello stranger");
  }
}

// "in" operator — check if property exists
type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    animal.swim();  // TS knows: Fish
  } else {
    animal.fly();   // TS knows: Bird
  }
}

// instanceof — for classes
function logDate(value: Date | string) {
  if (value instanceof Date) {
    console.log(value.toISOString());  // TS knows: Date
  } else {
    console.log(value);  // TS knows: string
  }
}

// Intersection — must be BOTH types
type HasName = { name: string };
type HasAge = { age: number };
type Person = HasName & HasAge;  // must have name AND age

const p: Person = { name: "John", age: 25 };  // Required!

// Exhaustiveness with switch
type Result = "success" | "error" | "pending";

function handle(result: Result) {
  switch (result) {
    case "success":
      console.log("Done!");
      break;
    case "error":
      console.log("Failed!");
      break;
    case "pending":
      console.log("Loading...");
      break;
    // If you forget a case, TypeScript ERRORS!
  }
}

Line-by-line walkthrough

  1. 1. Union type — string OR number
  2. 2. Declaring a variable
  3. 3.
  4. 4.
  5. 5. id = true; // ERROR!
  6. 6.
  7. 7. Literal union — only specific values
  8. 8. Defining a type alias
  9. 9. Declaring a variable
  10. 10. status = "deleted"; // ERROR!
  11. 11.
  12. 12. Using unions in functions
  13. 13. Declaring a function
  14. 14. Conditional check
  15. 15. Printing output to the console
  16. 16. Conditional check
  17. 17. Printing output to the console
  18. 18. Closing block
  19. 19. Closing block
  20. 20.
  21. 21. Truthiness narrowing
  22. 22. Declaring a function
  23. 23. Conditional check
  24. 24. name is string (not null)
  25. 25. Printing output to the console
  26. 26. Conditional check
  27. 27. Printing output to the console
  28. 28. Closing block
  29. 29. Closing block
  30. 30.
  31. 31. "in" operator — check if property exists
  32. 32. Defining a type alias
  33. 33. Defining a type alias
  34. 34.
  35. 35. Declaring a function
  36. 36. Conditional check
  37. 37.
  38. 38. Conditional check
  39. 39.
  40. 40. Closing block
  41. 41. Closing block
  42. 42.
  43. 43. instanceof — for classes
  44. 44. Declaring a function
  45. 45. Conditional check
  46. 46. Printing output to the console
  47. 47. Conditional check
  48. 48. Printing output to the console
  49. 49. Closing block
  50. 50. Closing block
  51. 51.
  52. 52. Intersection — must be BOTH types
  53. 53. Defining a type alias
  54. 54. Defining a type alias
  55. 55. Defining a type alias
  56. 56.
  57. 57. Declaring a variable
  58. 58.
  59. 59. Exhaustiveness with switch
  60. 60. Defining a type alias
  61. 61.
  62. 62. Declaring a function
  63. 63. Switch statement for multiple cases
  64. 64. Handling a specific case
  65. 65. Printing output to the console
  66. 66. Exiting the current case
  67. 67. Handling a specific case
  68. 68. Printing output to the console
  69. 69. Exiting the current case
  70. 70. Handling a specific case
  71. 71. Printing output to the console
  72. 72. Exiting the current case
  73. 73. If you forget a case, TypeScript ERRORS!
  74. 74. Closing block
  75. 75. Closing block

Spot the bug

function printId(id: string | number) {
  console.log(id.toUpperCase());
}
Need a hint?
Can you call toUpperCase() on a number?
Show answer
The function accepts string | number, but toUpperCase() only works on strings. If id is a number, it crashes. Fix: add typeof check: if (typeof id === 'string') { console.log(id.toUpperCase()); } else { console.log(id); }

Explain like I'm 5

Imagine a parking spot that fits either a car OR a bicycle. Before you try to drive it, you check: 'Is it a car or a bicycle?' That checking is called narrowing! You look at what something is before you use it so you don't try to pedal a car.

Fun fact

Type guards (the if statements that narrow types) are so important that TypeScript has built-in support for them. Miss a case? The compiler catches it!

Hands-on challenge

Create a union type for 'success' | 'error' | 'loading'. Write a function that takes this type and handles ALL three cases with a switch statement!

More resources

Open interactive version (quiz + challenge) ← Back to course: Full-Stack Playbook