Intersection Types
Intersection types combine several types into one. Write A & B and you get a type that has to satisfy A and B at the same time — every member of both, all at once. The ampersand is the entire syntax.
That much is easy. The confusion starts one step later, because & does not do the same thing to everything you feed it. Objects merge. Primitives collapse to never. Functions turn into overloads. Unions distribute. This guide walks through all four cases, the pitfalls that fail silently, and the point where you should have reached for | instead.
What Are Intersection Types
An intersection type is the logical AND of types. A value has to satisfy every included type to qualify.
type A = { a: number }
type B = { b: string }
type AandB = A & B
const obj: AandB = { a: 42, b: 'hello' }Here obj must contain both a and b. Think of it as wearing two hats at once. That is very different from a union (|), where you pick one hat or the other.
Order never matters. A & B and B & A are the same type, and TypeScript treats them as interchangeable everywhere.
The payoff is composition. Instead of writing one giant interface for every combination your app needs, you write small pieces and intersect the ones that apply.
Intersection Types vs Union Types: AND vs OR
A union type lets a value be one of several options. An intersection type demands all of them.
| Union | Intersection | |
|---|---|---|
| Reads as | A or B | A and B |
| Values that qualify | more | fewer |
| Members safe to access | only the shared ones | all of them |
| Conflicting property | keeps both branches | collapses to never |
| Reach for it when | a value is one of many | a value is all at once |
Here is the part that trips people up. For object types, the two operators move in the opposite direction from what the words suggest.
type Circle = { label: string; radius: number }
type Square = { label: string; side: number }
// Union: fewer members you can safely touch
declare const shape: Circle | Square
shape.label // ✅ present on both
// shape.radius ❌ not guaranteed to exist
// Intersection: more members required, all of them safe
declare const both: Circle & Square
both.label // ✅
both.radius // ✅
both.side // ✅An intersection of object types widens the set of required members while narrowing the set of values that qualify. A union does the reverse: more values qualify, fewer members are safe to access.
Swapping & for | does not always error
The dangerous case is the one that compiles either way. Change a single character and the type still type-checks — it just means something else.
type Admin = { role: 'admin'; permissions: string[] }
type Guest = { role: 'guest'; expiresAt: Date }
// Intended: a session is one or the other
type SessionOk = Admin | Guest
// Typo: & instead of |
type SessionBad = Admin & Guest
declare function audit(session: SessionBad): voidSessionBad is a perfectly legal type. But role has to be 'admin' and 'guest' at once, which no string can be, so that member is never and nothing will ever satisfy it. The compiler stays quiet at the declaration and only complains when someone calls audit, usually from a different file.
Discriminated unions are built for |. Intersecting their branches is almost always a typo. Use unions for alternatives, intersections for composition.
The Types of Intersections You Will Meet
& is one operator, but what it produces depends entirely on what you feed it. There are four cases worth knowing.
1. Object and object — the merge
This is the case everyone learns first. The members combine.
type WithId = { id: string }
type WithName = { name: string }
type Named = WithId & WithName // { id: string; name: string }2. Primitive and primitive — usually never
Two primitives with no values in common produce never, the type with no possible values.
[object Object]Nothing is both a string and a number, so there is nothing left. This is not an error on its own — TypeScript only complains when you try to assign something to it.
Literal types follow the same rule, which is occasionally useful:
type Yes = 'a' & string // 'a' — the literal survives
type No = 'a' & 'b' // never — no overlapWhat makes this hard to debug is that the collapse happens at the declaration while the complaint appears somewhere else.
type Millis = { unit: 'ms'; value: number }
type Seconds = { unit: 's'; value: number }
// Looks harmless. It is unusable.
type Either = Millis & Seconds
declare function schedule(delay: Either): void
// ❌ Type '"ms"' is not assignable to type 'never'
// schedule({ unit: 'ms', value: 1000 })The error points at the call, not at type Either. When a parameter refuses every value you give it, read the message for the word never, then hover each alias on the way back up. The first one that resolves to never instead of an object shape is where it collapsed.
3. Function and function — an overload
Intersecting two function types does not merge their signatures. It stacks them into an overloaded callable.
type Parse = ((input: string) => number) & ((input: number) => string)
declare const parse: Parse
const n = parse('42') // number
const s = parse(42) // stringTypeScript picks the first signature that matches the arguments, so order matters here in a way it does not for object intersections. Put the most specific signature first, or a broader one will swallow the calls you meant to route elsewhere. This is how built-in overloads like addEventListener are modelled under the hood.
4. Union and object — it distributes
When one side is a union, the intersection spreads across each member.
type Kind = { kind: 'circle' } | { kind: 'square' }
type Measured = Kind & { area: number }
// { kind: 'circle'; area: number } | { kind: 'square'; area: number }This is the trick behind adding a shared field to every branch of a discriminated union without rewriting each branch by hand.
Composing Object Types
The most common real-world use is merging smaller interfaces into richer models.
interface User {
id: number
username: string
}
interface Profile {
bio: string
avatar: string
}
type UserProfile = User & ProfileNow UserProfile has both identity and profile info without repeating definitions.
The pattern scales nicely with generics. A mixin that stamps bookkeeping fields onto any entity takes three lines, and it pairs well with mapped types when you want to transform a shape before intersecting it back onto a base.
type Timestamps = { createdAt: Date; updatedAt: Date }
type Entity<T> = T & Timestamps & { id: string }
type Post = Entity<{ title: string; body: string }>
// { title: string; body: string; createdAt: Date; updatedAt: Date; id: string }Intersections only ever add. When you need to go the other way and drop a field from a composed type, that is utility types territory — Omit<UserProfile, 'bio'> rather than another &.
Interfaces: & vs extends
For merging two interfaces, extends and & look interchangeable. They are not.
interface HasStatus {
status: string
}
// ❌ Error: types of property 'status' are incompatible
// interface Broken extends HasStatus {
// status: number
// }
// ✅ No error at declaration time — but 'status' is now never
type Quiet = HasStatus & { status: number }
// ❌ Type 'number' is not assignable to type 'never'
// const row: Quiet = { status: 200 }extends checks compatibility when you declare it and fails loudly on a conflict. & accepts the declaration and quietly collapses the conflicting member to never, so the error surfaces later at the assignment — often far from the cause.
Reach for extends when you want the compiler to catch mistakes early. Reach for & when you are composing types you do not control, or when you need to combine generic type parameters, which extends cannot do.
There is a readability difference too. An interface that extends another keeps its name in error messages and editor tooltips, so the compiler can tell you "Property 'status' is missing in type 'Draft'". A long intersection has no name of its own, so TypeScript prints the whole expanded shape instead. On deeply composed types that is the difference between a one-line error and a paragraph.
Enforcing Multiple Constraints
Sometimes a parameter needs to be more than one thing at once.
interface Logger {
log: (message: string) => void
}
interface Identified {
id: number
}
function logUser(user: Logger & Identified) {
user.log(`User ${user.id} logged in`)
}logUser only accepts objects that both log and carry an id, and the compiler checks that before the code runs.
The same move shows up constantly in UI code, where props arrive from several sources. Instead of one giant interface, intersect smaller ones.
type OwnProps = { label: string }
type StyleProps = { className?: string; style?: Record<string, string> }
type DispatchProps = { onSelect: (id: string) => void }
type Props = OwnProps & StyleProps & DispatchPropsEach group stays readable on its own, and a component that needs only two of them can intersect just those two. This is also how you extend a third-party component's props without editing its type: intersect its exported props type with your own.
Quirks and Pitfalls
- Conflicting properties fail silently. The declaration is legal; the member becomes
never. You find out at the assignment. - Optional loses to required.
{ name: string } & { name?: string }gives a requiredname: string. The stricter side always wins, which is usually what you want but rarely what people expect. - Excess property checks still apply. Assigning an object literal to an intersection is checked against the combined shape, so a typo in a key from either side is caught. Assign through a variable first and that check disappears, exactly as it does for a plain object type.
- Order is irrelevant for
&, but not for declaration merging. Two interfaces with the same name merge and conflict loudly; two types intersected with&never conflict at the declaration. - Every requirement is mandatory. Missing one property from one side is an error, even if the other five match.
- Deep intersections hurt. Long
&chains slow the compiler and produce hover tooltips nobody can read. Name the intermediate types.
Takeaway
Intersection types give you a way to say "this thing must have everything." They are ideal for merging objects, enforcing constraints, and structuring complex systems — as long as you remember that the four cases behave differently: objects merge, primitives collapse to never, functions overload, and unions distribute.
The practical rule is short. Reach for & when a value has to satisfy several contracts at once, and reach for | when it satisfies exactly one of several. If you find yourself intersecting types that were designed as alternatives, that is the signal you wanted a union all along.
Ready to practice? Work through the Intersection challenge, then try Union to Intersection or Parameter Intersection for the advanced versions.
Become a TypeScript Pro
Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges