---
title: 'Union and Intersection Types'
source: 'https://academia.sh/en/courses/typescript/union-and-intersection-types'
course: TypeScript
language: en
updated: '2026-08-17T18:09:54+00:00'
license: 'CC BY-SA 4.0'
---

# Union and Intersection Types

The shared-member rule in a union type, the discriminated union pattern, combining types with intersection, and the collapse of conflicting fields to the empty type.

The measurement record's `unit` field was written as `"C" | "Pa" | "%"`: a **union**
of three types that are alternatives to one another. The same operation also has an
intersection direction — values that carry the requirements of two types at once.

These two operators are the type-level counterpart of set algebra, and they frame the
measurement record's development from here on. This lesson covers which operations
can be done on a union, the discriminant field pattern, and when an intersection comes
up empty.

## Union: The Sum of Values

Type `A | B` covers all values of type `A` **or** type `B`. At the level of value
sets, this is a union operation:

$$
\text{values}(A \mid B) = \text{values}(A) \cup \text{values}(B)
$$

Code holding a union value does not know which member it came from. This is why it
can only perform operations that exist **in both members**:

```typescript
type NumericMeasurement = { id: string; value: number };
type TextualMeasurement = { id: string; text: string };
type Entry = NumericMeasurement | TextualMeasurement;

function write(entry: Entry): void {
  console.log(entry.id);
  console.log(entry.value);
}

write({ id: "s-01", value: 21.4 });
```

```text
e1.ts(7,21): error TS2339: Property 'value' does not exist on type 'Entry'.
  Property 'value' does not exist on type 'TextualMeasurement'.
```

Access to `id` is valid, because it exists in both members. Access to `value` is
rejected, because the value could be a `TextualMeasurement`. The diagnostic's second
line names which member is blocking it.

An inverse relationship follows from this, and it is surprising at first: **as the
value set grows, the usable member set shrinks.** Adding a new member to a union
reduces the operations that can be done with that type. In the language of sets: the
union of the values is the intersection of the guaranteed members.

## Discriminated Union

To access a member that exists in only one of a union's members, it has to be
**proven** which member the value came from. The most useful form of proof is putting
a field with the same name but a different literal type on every member:

```typescript
type Measurement =
  | { status: "valid"; id: string; value: number }
  | { status: "invalid"; id: string; code: string };

function summarize(measurement: Measurement): string {
  if (measurement.status === "valid") {
    return `${measurement.id}: ${measurement.value}`;
  }
  return `${measurement.id}: error ${measurement.code}`;
}

console.log(summarize({ status: "valid", id: "s-01", value: 21.4 }));
console.log(summarize({ status: "invalid", id: "s-02", code: "E17" }));
```

This file passes type checking and its output is:

```text
s-01: 21.4
s-02: error E17
```

The `status` field is called the **discriminant**; this pattern is called a
**discriminated union**. Three conditions are needed for it to work: the field's name
has to be the same across all members, its type has to be a literal type, and the
literals have to differ between members.

When these conditions hold, the compiler accepts the check
`measurement.status === "valid"` as proof and narrows the type to a single member
inside the `if` body. This narrowing is called **narrowing**, and its rules are the
subject of a separate lesson. Since `"invalid"` is the only member left after the `if`
block, access to `measurement.code` is valid there too.

At this point the measurement record's model makes its first structural decision: a
valid and an invalid record are not two states of the same type, they are two separate
types. This makes the `value` field accessible only in the state where it genuinely
exists — the question "does a value exist" has moved from runtime to compile time.

## Intersection: The Sum of Requirements

Type `A & B` covers values that are both `A` and `B`. For object types, this means
merging the fields:

```typescript
type Measurement = { id: string; value: number };
type Timestamped = { time: number };
type Entry = Measurement & Timestamped;

const entry: Entry = { id: "s-01", value: 21.4, time: 1706000000000 };
console.log(entry.id, entry.value, entry.time);

const missing: Entry = { id: "s-02", value: 22.1 };
```

```text
e2.ts(8,7): error TS2322: Type '{ id: string; value: number; }' is not assignable to type 'Entry'.
  Property 'time' is missing in type '{ id: string; value: number; }' but required in type 'Timestamped'.
```

The inverse relationship from the union case reverses here: an intersection's value
set is smaller, and its usable member set is larger. An `Entry` value carries every
field of both `Measurement` and `Timestamped`.

The typical use of intersection is keeping independently meaningful attributes in
separate types and combining them where needed. In the measurement record, a
timestamp, source information, and a validation result could each be defined
separately; different contexts use different combinations. This is the type-level
counterpart of the **abstract data type** idea introduced in the Data Structures
course: a type is defined less by the fields it carries than by the contracts it
satisfies.

## When an Intersection Collapses

Intersection does not always produce a meaningful type. Two primitive types have no
common value in their intersection:

```typescript
type Impossible = string & number;

const empty: Impossible = "s-01";
console.log(empty);
```

```text
e6.ts(3,7): error TS2322: Type '"s-01"' is not assignable to type 'never'.
```

Type `string & number` has been reduced to `never` — no value can be both a string and
a number. The diagnostic speaking directly of `never` is the result of this collapse.

The same thing shows up in object types too, through a conflicting field:

```typescript
type CelsiusRecord = { unit: "C"; value: number };
type PascalRecord = { unit: "Pa"; value: number };
type Both = CelsiusRecord & PascalRecord;

declare const record: Both;
const check: null = record;
const field: null = record.unit;
```

```text
e4.ts(7,28): error TS2339: Property 'unit' does not exist on type 'never'.
  The intersection 'Both' was reduced to 'never' because property 'unit' has conflicting types in some constituents.
```

The diagnostic states the reason for the collapse outright: because the types of the
`unit` field do not agree, the intersection has become `never` entirely. That the
sixth line raises no error is a consequence of the same thing — `never` is assignable
to every type.

This behavior is a design warning. If two types carrying conflicting fields need to be
combined, the right tool is not intersection but union: values follow the rule of one
or the other, not both at once.

## Comparing the Two Operators

| Criterion | Union `A \| B` | Intersection `A & B` |
|---|---|---|
| Value set | Union of sets `A` and `B` | Intersection of sets `A` and `B` |
| Accessible members | Only shared members | All members |
| Before use | Narrowing required | Not required |
| Conflicting field | Used as a discriminant | Collapses to the empty type |
| Typical use | The states a value can take | Combining independent attributes |

The table shows the two operators do not substitute for each other. Union is for
modeling a **choice**: a record is either valid or invalid. Intersection is for
modeling a **sum**: a record has both an id and a timestamp.

## Summary

- A union type's value set is the union of its members; its accessible member set is
  the intersection of its members.
- A discriminated union puts a field with the same name and a different literal type
  on every member; a check on that field enables narrowing.
- An intersection type merges object types' fields and imposes all requirements at
  once.
- The intersection of two primitive types, and of object types carrying a conflicting
  field, collapses to `never`; the compiler states the reason in the diagnostic.
- Union models a choice, intersection models a sum; for conflicting fields, the right
  tool is union.

## Next Step

For the discriminated union to work, the fields had to be of a literal type. Yet as
seen in the type inference lesson, an object literal's fields are widened on their
own and the literal type is lost. The next lesson covers the tools that stop this
widening — literal types, the `as const` notation, and the `satisfies` operator that
checks a type without narrowing it.
