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).
Can use: properties in both
Need narrowing to access either side
e.g.
string | number
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.”