Intersection Types

September 26, 202510 min read
Requirements:
FunctionsObjects| Unions

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.

UnionIntersection
Reads asA or BA and B
Values that qualifymorefewer
Members safe to accessonly the shared onesall of them
Conflicting propertykeeps both branchescollapses to never
Reach for it whena value is one of manya 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): void

SessionBad 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 overlap

What 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) // string

TypeScript 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 & Profile

Now 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 & DispatchProps

Each 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

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.

Share this article

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

Practice with Challenges

Put your intersection types knowledge to the test with these related challenges.

#645Diff
Medium
#599Merge
Medium
#27932MergeAll
Medium
#5821MapTypes
Medium
#21106Combination key type
Medium
#5423Intersection
Hard
#55Union to Intersection
Hard

Related Concepts

Concepts that build on or relate to intersection types.

Union TypesInterfacesMapped TypesTypeScript Function Types