Union & Intersection Types

intermediate union intersection composition

Two operators sit at the heart of TypeScript’s type system: | (union) and & (intersection). They look like JS bitwise operators but mean something very different here.

Union — A | B means “either A or B”

A value of type A | B could be either. We can only use properties/methods that exist on BOTH sides until we narrow.

type ID = string | number;

function printId(id: ID) {
  // id.toUpperCase(); // ❌ not all IDs have this
  if (typeof id === "string") {
    console.log(id.toUpperCase()); // ✅ narrowed to string
  } else {
    console.log(id.toFixed(2));    // ✅ narrowed to number
  }
}

Unions are everywhere — function parameters that accept multiple shapes, return types that can be a value or null, state machines.

type LoadState = "idle" | "loading" | "success" | "error";

Intersection — A & B means “both A and B at once”

A value of type A & B has ALL properties from both. Think of it as merging the shapes.

type HasId = { id: number };
type HasName = { name: string };

type User = HasId & HasName;
// User has both id AND name

const u: User = { id: 1, name: "Manish" }; // both required

In simple language — union narrows what we can do (only common stuff), intersection broadens what we have (everything from both).

A | B (union, OR)
Value is A or B
Can use: properties in both
Need narrowing to access either side
e.g. string | number
A & B (intersection, AND)
Value is A and B
Can use: properties from either
No narrowing needed
e.g. HasId & HasName

Combining unions and intersections

Real-world types often mix both. A common pattern is “base props + variant”.

type ButtonBase = { onClick: () => void };
type Primary = { variant: "primary"; label: string };
type Icon = { variant: "icon"; icon: string };

type ButtonProps = ButtonBase & (Primary | Icon);
// every button has onClick, plus EITHER label OR icon depending on variant

Intersection of primitives — never

Intersecting incompatible primitives gives never (no value can be both).

type Weird = string & number; // never

Common mistake: union of objects ≠ intersection of properties

When we have A | B where both are objects, we can only access properties present in BOTH.

type A = { x: number; y: number };
type B = { x: number; z: number };

function f(v: A | B) {
  v.x; // ✅ in both
  // v.y; // ❌ only in A
}

That’s where discriminated unions come in — they let us narrow cleanly. See the next note.

Narrowing unions

We narrow with typeof, instanceof, in, equality checks, or user-defined type guards. See the Type Narrowing note for the full story.

function area(shape: { kind: "circle"; r: number } | { kind: "square"; s: number }) {
  if (shape.kind === "circle") return Math.PI * shape.r ** 2;
  return shape.s ** 2;
}

Interview soundbite

| is union, an OR — value is one of the listed types and we narrow before using specifics. & is intersection, an AND — value has everything from both types. Unions need narrowing; intersections combine shapes.”