Learn TypeScript w/ Mike North

Union and Intersection Types

June 08, 2021

Table of Contents

Union and Intersection Types, Conceptually

Union and intersection types can conceptually be thought of as logical boolean operators (AND, OR) as they pertain to types. Let’s look at this group of two overlapping sets of items as an example:

union

A union type has a very specific technical definition that comes from set theory, but it’s completely fine to think of it as OR, for types.

In the diagram above, if we had a type Fruit OR Sour it would include every one of the items on the entire chart.

Intersection types also have a name and definition that comes from set theory, but they can be thought of as AND, for types.

In the same diagram above, if we wanted fruits that are also sour (Fruit AND Sour) we’d end up only getting { Lemon, Lime, Grapefruit }.

Union Types in TypeScript

Union types in TypeScript can be described using the | (pipe) operator.

For example, if we had a type that could be one of two strings, "success" or "error", we could define it as

ts
"success" | "error"

For example, the flipCoin() function will return "heads" if a number selected from (0, 1) is >= 0.5, or "tails" if <=0.5.

ts
function flipCoin(): "heads" | "tails" {
if (Math.random() > 0.5) return "heads"
return "tails"
}
 
const outcome = flipCoin()
const outcome: "heads" | "tails"
Try

Let’s make this a bit more interesting by using tuples, that is structured as follows:

  • [0] either "success" or "failure"
  • [1] something different, depending on the value found in [0]

    • "success" case: a piece of contact information: { name: string; email: string; }
    • "error" case: an Error instance

We’ll still decide which of these things actually happens based on our 50/50 coin flip from above

ts
function flipCoin(): "heads" | "tails" {
if (Math.random() > 0.5) return "heads"
return "tails"
}
 
function maybeGetUserInfo():
| ["error", Error]
| ["success", { name: string; email: string }] {
if (flipCoin() === "heads") {
return [
"success",
{ name: "Mike North", email: "mike@example.com" },
]
} else {
return [
"error",
new Error("The coin landed on TAILS :("),
]
}
}
 
const outcome = maybeGetUserInfo()
const outcome: ["error", Error] | ["success", { name: string; email: string; }]
Try

this type is significantly more interesting.

Working with union types

Let’s continue with our example from above and attempt to do something with the “outcome” value.

First, let’s destructure the tuple and see what TypeScript has to say about its members

ts
const outcome = maybeGetUserInfo()
 
const [first, second] = outcome
first
const first: "error" | "success"
second
const second: Error | { name: string; email: string; }
Try
emoji-bulb A good time to poke around

Click the Try button and explore first and second in the TS playground. Explore what’s available in the autocomplete for each.

ts
const outcome = maybeGetUserInfo()
const [first, second] = outcome
first.split
       
second.name
        
Try

We can see that the autocomplete information for the first value suggests that it’s a string. This is because, regardles of whether this happens to be the specific "success" or "error" string, it’s definitely going to be a string.

The second value is a bit more complicated — only the name property is available to us. This is because, both our “user info object, and instances of the Error class have a name property whose value is a string.

What we are seeing here is, when a value has a type that includes a union, we are only able to use the “common behavior” that’s guaranteed to be there.

Narrowing with type guards

Ultimately, we need to “separate” the two potential possibilities for our value, or we won’t be able to get very far. We can do this with type guards.

Type guards are expressions, which when used with control flow statement, allow us to have a more specific type for a particular value.

I like to think of these as “glue” between the compile time type-checking and runtime execution of your code. We will work with one that you should already be familiar with to start: instanceof.

ts
const outcome = maybeGetUserInfo()
const [first, second] = outcome
const second: Error | { name: string; email: string; }
if (second instanceof Error) {
// In this branch of your code, second is an Error
second
const second: Error
} else {
// In this branch of your code, second is the user info
second
const second: { name: string; email: string; }
}
Try

TypeScript has a special understanding of what it means when our instanceof check returns true or false, and creates a branch of code that handles each possibility.

It gets even better…

Discriminated Unions

ts
const outcome = maybeGetUserInfo()
if (outcome[0] === "error") {
// In this branch of your code, second is an Error
outcome
const outcome: ["error", Error]
} else {
// In this branch of your code, second is the user info
outcome
const outcome: ["success", { name: string; email: string; }]
}
Try

TypeScript understands that the first and second positions of our tuple are linked. What we are seeing here is sometimes referred to as a discriminated or “tagged” union type.

Intersection Types in TypeScript

Intersection types in TypeScript can be described using the & (ampersand) operator.

For example, what if we had a Promise, that had extra startTime and endTime properties added to it?

ts
function makeWeek(): Date & { end: Date } {
//⬅ return type
 
const start = new Date()
const end = new Date(start.valueOf() + ONE_WEEK)
 
return { ...start, end } // kind of Object.assign
}
 
const thisWeek = makeWeek()
thisWeek.toISOString()
const thisWeek: Date & { end: Date; }
thisWeek.end.toISOString()
(property) end: Date
Try

This is quite different than what we saw with union types — this is quite literally a Date and { end: Date} mashed together, and we have access to everything immediately.

It is far less common to use intersection types compared to union types. I expect it to be at least a 50-to-1 ratio for you in practice.

emoji-grey_question Ask yourself: why might you run into union types more often?
  • Consider control flow and function return types


© 2022 All Rights Reserved