Skip to Content
Course content

25: Union and Intersection Types

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've been working on a notification system for a client, and I ran into a classic TypeScript architectural headache. The system needs to handle different types of messages—some are just emails, some are just SMS texts, and a few are "Critical Alerts" that actually require both. Let's walk through how I solved this, because it's the perfect way to see where Union and Intersection types actually fit in.

The "Either-Or" Dilemma

First, I defined my basic data structures. Pretty straightforward stuff.

type Email = {
  emailAddress: string;
  subject: string;
};

type SMS = {
  phoneNumber: string;
  message: string;
};

Now, I wanted a function that could process a notification regardless of whether it was an email or an SMS. My first instinct was to just pick one, but that doesn't make sense. I need it to be flexible. So, I tried this:

function sendNotification(config: Email | SMS) {
  console.log("Sending notification...");
  // Let's try to log the destination
  console.log(config.emailAddress); 
}

Immediately, TypeScript flags config.emailAddress with a red squiggly line. It tells me that emailAddress doesn't exist on type SMS. Which makes sense! If I pass an SMS object into this function, there is no email address to log.

This is the core of a Union Type (the | symbol). I'm telling TypeScript: "This variable could be an Email OR it could be an SMS." The catch is that TypeScript will only let me access properties that are common to both types. Since emailAddress and phoneNumber are unique to their respective types, I can't just grab them blindly.

Narrowing the Path

To fix this, I can't just tell TypeScript to "trust me." I have to prove which type I'm dealing with. I'll use a check to narrow the type down.

function sendNotification(config: Email | SMS) {
  if ('emailAddress' in config) {
    // Inside this block, TS knows config is an Email
    console.log(`Sending email to ${config.emailAddress}`);
  } else {
    // Since it's not an Email, it must be an SMS
    console.log(`Sending SMS to ${config.phoneNumber}`);
  }
}

That works. By using the in operator, I've narrowed the Union. It's a common pattern: start broad with a Union, then use conditional logic to get specific.

When "Or" Isn't Enough

Then the requirements changed. The client wanted "Critical Alerts." These aren't just emails or SMS; they are a hybrid. A critical alert must have the properties of an Email AND the properties of an SMS to ensure the user is reached by all available means.

I could create a third type and manually copy all the fields, but that's a maintenance nightmare. If I add a cc field to Email later, I'd have to remember to add it to CriticalAlert too. Instead, I'll try an Intersection Type (the & symbol).

type CriticalAlert = Email & SMS;

const emergency: CriticalAlert = {
  emailAddress: "admin@system.com",
  subject: "SERVER DOWN",
  phoneNumber: "555-0199",
  message: "Immediate action required!"
};

Notice the difference. While the Union (|) was an "Either/Or" situation, the Intersection (&) is an "And" situation. A CriticalAlert is now a type that possesses every single property of Email AND every single property of SMS. If I forgot to include the phoneNumber in the emergency object, TypeScript would throw an error immediately.

Choosing the Right Tool

It's easy to mix these up when you're staring at code at 2 AM. Here is how I keep them straight in my head:

  • Unions (|): Use these when a value can be one of several different things. It expands the possibilities of what the value can be, but restricts what properties you can access until you narrow the type.
  • Intersections (&): Use these when you want to combine multiple types into one. It restricts the possibilities (the object must satisfy all criteria) but expands the properties available to you.



📋 Practical Task

Building a User Permission System

You are building a permission system for a CMS. You have two base types: Guest and Admin.

type Guest = {
  canView: boolean;
};

type Admin = {
  canEdit: boolean;
  canDelete: boolean;
};

Your task is to implement the following:

  1. Create a Union type called User that can be either a Guest or an Admin.
  2. Create an Intersection type called SuperUser that combines Guest and Admin.
  3. Write a function called checkPermissions that accepts a User. Inside the function, use a type guard (like the in operator) to log "Admin access granted" if the user is an Admin, and "Guest access granted" otherwise.
  4. Create a SuperUser object and verify that you can access canView, canEdit, and canDelete on it without any TypeScript errors.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.