open atlas
↑ Back to track
TypeScript type system, deep TS · 02 · 01

Unions and intersections

Unions mean one-of and only expose common members; intersections mean all-of and merge object members. Mix incompatible primitives and you get never.

TS Junior ◷ 13 min
Level
FoundationsJuniorMiddleSenior

A teammate writes type Handler = ClickHandler | KeyHandler and then tries to call handler.preventDefault() — a method on only one of them. The checker refuses: “Property ‘preventDefault’ does not exist on type ‘ClickHandler | KeyHandler’.” They “fix” it by switching to ClickHandler & KeyHandler. Now the error is gone, but every call site that passed a plain ClickHandler breaks. The two operators look symmetric. They are opposites — and confusing them is one of the most common type errors in real code.

One-of versus all-of

When you reach for | or &, ask yourself: can the caller provide one thing, or must they provide everything? The answer determines which operator you need — and using the wrong one compiles fine while silently breaking type safety.

A union A | B describes a value that is either an A or a B. You do not know which one statically, so the only members the checker lets you touch are the ones present in both.

An intersection A & B describes a value that is simultaneously an A and a B. It has every member of both, because it must satisfy both contracts at once.

type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; side: number };

type Shape = Circle | Square;
//   ^? type Shape = Circle | Square

function describe(s: Shape) {
  s.kind;
  // ^? (property) kind: "circle" | "square"   — common to both, OK
  s.radius;
  // Error: Property 'radius' does not exist on type 'Shape'.
  //   Property 'radius' does not exist on type 'Square'.
}

kind exists on both members, so it is reachable and its type is the union of the two literal types. radius exists only on Circle, so on the union it is unreachable until you narrow (next lesson). This is the whole reason narrowing exists.

Intersection merges object members

For object types, A & B is a new type carrying all properties of both. It is the natural way to “add fields” to a shape.

type WithId = { id: string };
type WithTimestamps = { createdAt: Date; updatedAt: Date };

type Entity = WithId & WithTimestamps;

const row: Entity = { id: "u1", createdAt: new Date(), updatedAt: new Date() };
//    ^? const row: Entity
// Drop any one field and the checker complains:
const bad: Entity = { id: "u1", createdAt: new Date() };
// Error: Property 'updatedAt' is missing in type
//   '{ id: string; createdAt: Date; }' but required in type 'WithTimestamps'.

If both sides declare the same property with compatible types, the result keeps it. If they declare the same property with incompatible types, the merged property collapses to never — see below.

Why this works

Why “all-of” for an intersection feels backwards at first: in set theory the intersection of two sets is smaller. But TypeScript types are sets of values, and a value type like { a: number } is the set of all objects that have at least an a. The set of objects that have at least a and at least b is smaller than either alone — yet each such value carries more members. Fewer values, more guaranteed fields. That is why A & B is a narrower value set but a wider member set.

Incompatible primitives intersect to never

never is the empty type — the set of zero values. No value can be both a string and a number at the same time, so the checker reduces the impossible intersection to never:

type Impossible = string & number;
//   ^? type Impossible = never

type AlsoNever = "a" & "b";
//   ^? type AlsoNever = never

This is silent. It does not error at the type alias — it errors only when you later try to assign something to it, because nothing is assignable to never:

const x: string & number = "hi";
// Error: Type 'string' is not assignable to type 'never'.

The dangerous version hides inside object intersections. If two object types share a key whose value types are disjoint, only that property becomes never, and the object type as a whole still looks usable:

type A = { status: "active"; v: number };
type B = { status: "archived"; v: number };

type Both = A & B;
//   ^? type Both = A & B
type S = Both["status"];
//   ^? type S = never   — "active" & "archived" is impossible

Both is uninhabitable, but you only discover it when you try to construct a value or read status. This is the classic trap of reaching for & when you meant a union.

Complete the analogy

A union A | B is like a function parameter that accepts either a cat or a dog. You may only call the methods that ___ animals share.

Behaviour at function parameters

A function typed to accept a union must handle every member, so its body may use only common members. A function typed to accept an intersection is easier to satisfy inside the body (more members guaranteed) but harder to call (the argument must be both):

declare function logUnion(x: number | string): void;
logUnion(1);        // OK
logUnion("a");      // OK

declare function logBoth(x: { a: 1 } & { b: 2 }): void;
logBoth({ a: 1 });
// Error: Property 'b' is missing in type '{ a: 1; }'
//   but required in type '{ b: 2; }'.
logBoth({ a: 1, b: 2 }); // OK

Note the asymmetry in assignability: a number is assignable to number | string (a part fits the whole-or), but you cannot pass a partial object to an & parameter (the whole-and demands everything).

interface extends versus A & B

Both compose object shapes, but they are not interchangeable. interface Child extends Parent {} checks for conflicts eagerly and errors at declaration. Intersection defers and silently produces never on conflict:

interface Base { v: string }
interface Wrong extends Base { v: number }
// Error: Interface 'Wrong' incorrectly extends interface 'Base'.
//   Types of property 'v' are incompatible. Type 'number' is not assignable to type 'string'.

type AlsoWrong = Base & { v: number };
//   ^? type AlsoWrong = Base & { v: number }   — no error here…
type V = AlsoWrong["v"];
//   ^? type V = never   — …the conflict surfaces only when you read it

Prefer extends for nominal “is-a” hierarchies that you want validated at the declaration site; reach for & to mix orthogonal, conflict-free aspects (mixins, “add timestamps”, “add an id”). For unions that vary by a tag, you will model them as discriminated unions later in this unit.

Reach the right member

1/3
Quiz

Given `type X = { id: string } | { id: string; email: string }`, which property is reachable on a value of type X without narrowing?

Quiz

What is `string & number` in TypeScript, and when does the problem surface?

Recall before you leave
  1. 01
    Why can you access only common members on a union, and what unlocks the rest?
  2. 02
    Explain why `A & B` carries more members but describes fewer values, using set-of-values reasoning.
  3. 03
    How do incompatible types behave under intersection, and why is the object-property case a trap?
Recap

Unions and intersections are the two ways to compose types, and they are opposites. A union A | B is one-of — a value that is one of its members, exposing only the members common to all of them, which is precisely why you will need narrowing to reach the rest. An intersection A & B is all-of — for object types it merges every member, requiring all of them at construction; disjoint primitives like string & number collapse silently to the empty type never, and the same happens per-property when object types share a key with incompatible value types. Prefer interface extends for validated is-a hierarchies and & for conflict-free mixins. Next, narrowing shows how the checker tracks which union branch you are in at each point in the program, turning that hidden union member into a reachable one. Now when you see a “Property does not exist” error on a union, you will know whether to reach for narrowing or rethink whether | was the right operator to begin with.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 4 done
Connected lessons

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.