📖 The Story: The Format Fiasco

A backend API was updated to support both string IDs ("user_123") and numeric IDs (123). The frontend developer wrote a function to display the ID: function printId(id: string) { console.log(id.toUpperCase()) }.

Immediately, the app crashed when a numeric ID was passed. The developer tried to fix it by changing the parameter to id: any, but then id.toUpperCase() crashed on numbers because numbers don't have that method.

The solution was a Union Type. By defining the parameter as id: string | number, TypeScript forced the developer to check if id was a string before calling .toUpperCase(). The crash was prevented before the code was even run.

Sometimes data can be multiple things. Union types make TypeScript aware of the possibilities.

🎯 Why Combine Types?

🤝

Union (OR)

Allow a variable to be one of several types (e.g., string or number).

🔗

Intersection (AND)

Combine multiple types into one super-type that has all properties.

🛡️

Safe Narrowing

Force developers to check the type before using type-specific methods.

🧒 Explain Like I'm 10

Imagine you are ordering a combo meal. You can choose a burger OR a hot dog (Union). But no matter what you choose, it comes WITH fries AND a drink (Intersection).

A union type is the choice (Burger | HotDog). An intersection type is the full combo plate (Burger & Fries & Drink).

🎓 Professional Explanation

A Union Type uses the | operator to say a value can be one of several types. An Intersection Type uses the & operator to combine multiple types into one. When you use a union type, TypeScript requires you to narrow the type (using typeof, in, or instanceof) before you can access properties specific to one of the types.

🤝 Union Types (OR)

typescript
function printId(id: string | number) {
  console.log("Your ID is: " + id);
}

printId(101); // OK
printId("202"); // OK
printId(true); // Error: boolean not assignable

🔍 Type Narrowing

If you have a union type, you can't immediately access methods specific to a string or a number. You must narrow it down.

typescript
function formatId(id: string | number) {
  // Error: Property 'toUpperCase' does not exist on type 'string | number'.
  // console.log(id.toUpperCase()); 
  
  // Narrowing using typeof
  if (typeof id === "string") {
    console.log(id.toUpperCase()); // TS knows it's a string here!
  } else {
    console.log(id.toFixed(2)); // TS knows it's a number here!
  }
}

The in operator is used to narrow objects: if ("isAdmin" in user) { ... }.

🔗 Intersection Types (AND)

Intersections combine multiple types. The result must satisfy all combined types.

typescript
interface BusinessPartner {
  name: string;
  creditLimit: number;
}

interface Contact {
  email: string;
  phone: string;
}

// Must have name, creditLimit, email, AND phone
type Vendor = BusinessPartner & Contact;

const myVendor: Vendor = {
  name: "Acme Corp",
  creditLimit: 10000,
  email: "sales@acme.com",
  phone: "555-1234"
};

📜 Literal Types

Unions are often used with literal types to define exact values a variable can hold.

typescript
type Direction = "North" | "South" | "East" | "West";

function move(direction: Direction) {
  // ...
}

move("North"); // OK
move("Up"); // Error

⚠️ Common Mistakes

Mistake 1: Assuming Union Means "Any Property"

If you have TypeA | TypeB, you can only access properties that exist on both TypeA and TypeB. To access a property unique to TypeA, you must narrow it first.

✅ Best Practices

  1. Use Discriminated Unions: Give objects a common property (like type: 'success') to make narrowing easier.
  2. Avoid intersecting incompatible types: Intersecting string & number results in never.

🏗️ Senior Deep Dive

Discriminated Unions (The Holy Grail)

This is the most powerful pattern in TypeScript for managing state (like Redux or API responses).

typescript
type NetworkState = 
  | { state: "loading" }
  | { state: "failed", error: string }
  | { state: "success", data: string };

function logger(state: NetworkState) {
  switch (state.state) {
    case "loading":
      return "Loading...";
    case "failed":
      return state.error; // TS knows 'error' exists here!
    case "success":
      return state.data; // TS knows 'data' exists here!
  }
}

💼 Interview Questions

Beginner
What is the difference between | and & in TypeScript?
Show Answer
| creates a Union type (OR logic), allowing a value to be one of several types. & creates an Intersection type (AND logic), combining multiple types into one that must have all properties.
Intermediate
What is Type Narrowing?
Show Answer
Type narrowing is the process of refining a union type to a specific type within a conditional block (using typeof, in, or instanceof), allowing safe access to type-specific properties.

🏋️ Practical Exercise

🎯 Hands-On Practice Medium

Write the Narrowing Logic

Given this function, write the if statement to safely call .toFixed(2) if the value is a number.

typescript">
function processValue(value: string | number) {
  // Write your if statement here
}

📝 Quiz

Test your understanding. Click an option to check your answer.

1. Which operator is used to create an Intersection Type (AND logic)?
A) |
B) &
C) &&
Answer: B — The single ampersand (&) is used to combine types.

📦 Summary

🎯 Key Takeaways

  • Union Types (|) allow a variable to be one of several types.
  • Intersection Types (&) combine multiple types into one.
  • You must narrow union types using typeof or in before accessing specific properties.
  • Discriminated Unions are the best pattern for managing complex state.