Intersection Types
`A & B` is a value that satisfies both constraints at once. It is the right tool for mixins and for refining an over-broad type, and it will cheerfully let you write a type that no value can ever have.
What does A & B actually give me, and why does string & number type-check when nothing can ever be one?
A type denoting the intersection of its members' value sets — a value usable at every member type simultaneously. Internally the checker keeps it as the list of members plus, for object types, a lazily computed merged member table; for call signatures it keeps an ordered overload set rather than merging anything. The representation exists to answer "which operations are available", and it answers that by union of members even though the value set is an intersection.
A value may be given type A & B only if it satisfies every constraint of every member. The checker may merge member tables only where members sharing a name have a common subtype; where they do not, the merged property is uninhabited and the whole type becomes unsatisfiable. Constructing such a type is legal — the error, if any, appears at the attempt to produce a value, which may be nowhere at all.
Key points
- An intersection narrows the value set and widens the operation set — the exact mirror of a union.
- For object types the members merge; for call signatures they form an ordered overload set rather than merging.
- Mixins, middleware-style type refinement and multi-capability generic constraints are the three uses that genuinely need intersections.
- An intersection may be uninhabited, and that is well-formed rather than an error — the empty type is a real and useful type.
- Conflicting object properties do not usually reduce the whole type to
never; they produce a property of typenever, so the error appears at construction, far from the declaration. - Intersections are cheap to write, costly to check, and produce some of the worst error messages a checker emits.
Combining constraints, not combining values
An intersection is a conjunction. Serializable & Comparable describes values that are both, so every operation of both is available on it. This is the mirror image of [[union-types]]: a union has fewer values excluded and therefore fewer operations available; an intersection has more values excluded and therefore more operations available. The set arithmetic and the operation arithmetic always run in opposite directions, and the cardinality argument in [[algebraic-data-types]] is where that stops being a coincidence.
For object types the merge is what you would expect: { a: number } & { b: string } has both properties. For function types it is not a merge at all — ((x: string) => void) & ((x: number) => void) is an overload set, resolved by trying the signatures in order. That asymmetry catches people, and it exists because there is no single signature that means "accepts a string and separately accepts a number"; the only faithful representation is the list.
An intersection is also not the same as inheritance, even though both produce a type with everyone's members. Inheritance declares a nominal edge that survives into the runtime representation and into instanceof; an intersection declares nothing and exists only in the checker. In a nominal language the two are barely comparable — see [[structural-vs-nominal]].
| Axis | `A | B` | `A & B` |
|---|---|---|
| Value set | Values of A together with values of B — larger. | Values that are both A and B — smaller. |
| Available operations | Only those defined on every member — fewer. | Those defined on any member — more. |
| Assignability direction | A is assignable to A | B. | A & B is assignable to A. |
| Degenerate case | A | never is A. A union of everything is unknown. | A & never is never. An intersection of disjoint primitives is uninhabited. |
| Cardinality | Adds: |A| + |B| for disjoint members. | Intersects: often 0, and 0 is a perfectly well-formed type. |
| Typical use | Describing data whose shape varies. | Describing a capability requirement, or refining an over-broad type. |
Where intersections genuinely earn their place
The first honest use is mixins. A mixin is a function that takes a class and returns a class with more members; the return type is naturally the intersection of what went in and what was added, because that is literally what the value is. Trying to express the same thing with inheritance requires a fresh named class per combination, which is combinatorial; the intersection is compositional and costs one type expression.
The second is refining a type you do not control. A library hands you Request; your middleware attaches a user. The type of the object after the middleware is Request & { user: User } — no new declaration, no fork of the library's types, and every existing function that accepts a Request still accepts it. This is the standard shape of typed middleware and it is the reason intersections exist in a structural system at all.
The third is capability constraints on a type parameter. function save<T extends Serializable & HasId>(x: T) says the argument must satisfy two independent requirements which no single interface in the codebase happens to combine. Declaring a SerializableHasId interface would work too, and would then need every implementor updated — which is exactly the versioning cost that makes the structural, anonymous version attractive.
What all three have in common: the intersection is describing a value that genuinely exists and genuinely satisfies both. When you find yourself intersecting types to make an error go away rather than to describe a value, the type is about to become uninhabited.
1type Ctor<T = {}> = new (...args: any[]) => T2 3function Timestamped<B extends Ctor>(Base: B) {4 return class extends Base {5 createdAt = new Date()6 }7}8 9class User { constructor(public name: string) {} }10const TimestampedUser = Timestamped(User)11 12// The instance type is User & { createdAt: Date } — not a declared class anywhere.13const u = new TimestampedUser("ada")14u.name // from User15u.createdAt // from the mixinNo name was invented for the combined type, and none needs to be. With inheritance, three mixins over four base classes needs twelve declarations; with intersections it needs zero.
Uninhabited intersections, and why they are not errors
never, and the non-reduction of conflicting object properties, is TypeScript-specific behaviour (present since 3.9 for the primitive case) and has changed across versions. Flow reports some of these at the declaration; Scala 3, whose A & B is a genuine greatest-lower-bound in a nominal-with-structural-refinements system, reduces differently again. Do not carry the reduction rules to another checker.Write type T = string & number and nothing complains. There is no value that is simultaneously a string and a number, so the type has no inhabitants — and a type with no inhabitants is a perfectly ordinary thing for a type system to have. It is called never in TypeScript, ! in Rust, Nothing in Scala, Void in Haskell, and it is genuinely useful: it is the return type of a function that does not return, and it is what makes the exhaustiveness trick in [[exhaustiveness-checking]] work.
The reason it is not reported at the declaration is that the checker cannot generally decide inhabitation. For primitives it can and does — TypeScript reduces an intersection of provably disjoint primitives to never eagerly. For object types it does not: { a: string } & { a: number } is not reduced to never, it becomes an object type whose a has type string & number, which is never. The object type still has a name, still has a property, and still cannot be constructed. The error, when it comes, comes at the construction site and says something about a rather than about the type as a whole.
This is where intersections turn into a debugging problem. The type expression that is impossible is often several aliases away from the assignment that fails, and the diagnostic points at the assignment. The habit that fixes it is to ask, whenever an intersection surprises you, whether any value could have that type — and to check by trying to write one.
The pathological version is intersecting types with the same method at incompatible signatures. You get an overload set that no implementation can satisfy, the type is fine, every call site resolves, and the only thing that fails is the one place someone tries to implement it.
1type A = string & number2// => never, reduced eagerly: primitives are provably disjoint3 4type B = { id: string } & { id: number }5// => { id: never } — not reduced. The type exists; no value does.6const b: B = { id: "x" } // error lands here, about `id`, not about B7 8type C = { f(x: string): void } & { f(x: number): void }9// => f is an overload set. Callers are fine; implementors are not.10const c: C = { f(x: string) {} } // error: does not satisfy the number overloadThe three cases fail at three different places — declaration, first construction, first implementation — and only the first is reported where it was written.
What an intersection costs the checker
Intersections are cheap to write and expensive to check. Deciding whether X is assignable to A & B means deciding it against every member; deciding whether A & B is assignable to X means finding *some* combination of members that covers X, which is a search. Combine that with generics and conditional types and the checker is doing real work on every use — this is a well-known source of slow builds in large TypeScript codebases, and the fix is almost always to name the resulting type once rather than re-deriving the intersection at every site.
The diagnostic cost is worse than the time cost. An error against an intersection has to explain which member failed and why, and the natural rendering of that is to print all the members. A four-member intersection of generic object types produces an error message measured in screenfuls, and the useful line is somewhere in the middle. This is the diagnostic-quality problem from [[diagnostic-quality]] in its purest form: the checker knows exactly what is wrong and cannot say it briefly.
How it works
The steps, in the order the compiler takes them.
- The checker normalises the intersection: flattens nested intersections, removes duplicates, and reduces to
neverwhere two members are provably disjoint primitives. - For an assignability query *into* the intersection, it checks the source against every member and requires all to succeed.
- For an assignability query *out of* the intersection, it succeeds if any member satisfies the target, which makes the query a search rather than a walk.
- For property access, it looks the name up in each member and, if found in more than one, forms the intersection of the property types — which is where
neverproperties come from. - For call signatures, it collects them into an overload list in member order and resolves calls by trying each in turn, taking the first that matches.
- For a generic constraint
T extends A & B, inference must find aTsatisfying all members, so a failure reports against the whole conjunction and the diagnostic enumerates it.
How it breaks
What the engineer observes when it goes wrong — not what goes wrong internally.
- An intersection is declared, everything type-checks, and the first attempt to construct a value fails with an error naming a property whose type has become
never— several aliases away from the intersection that caused it. - A mixin chain grows to five layers and the editor's hover on a variable prints a type that does not fit on the screen, so nobody reads it and the actual shape of the object stops being known to the team.
- Two intersected interfaces both declare a method with different signatures; every call site resolves against one overload and the single implementation site is the only thing that fails, months later, when someone tries to write it.
- Build times climb steadily as intersections accumulate in shared type utilities, and the profile shows the checker re-deriving the same intersection at hundreds of call sites because it was never given a name.
- A middleware attaches a property and types the result as an intersection, but a handler further down receives the base type from a different path — the property is missing at runtime while the types said it was there.
When it helps
- Mixin and decorator patterns, where the combined type is genuinely the conjunction and enumerating names would be combinatorial.
- Adding fields to a third-party type at a boundary without forking its declarations — the standard typed-middleware shape.
- Expressing a generic constraint that requires two independent capabilities that no single declared interface combines.
- Narrowing an over-broad library type at one place, so the rest of the code sees the refined form without casts.
When it hurts
- As a way to silence an assignability error. If you did not have a value in mind that satisfies both members, you have just written a type nothing can inhabit.
- Deeply nested in generic utility types, where every use site re-derives it and the error messages become unreadable.
- Across members with overlapping property names, where the merge is silently producing
neverproperties that only surface at construction. - In a nominal language, where the construct usually does not exist and the equivalent — declaring the combined interface — is the idiomatic and cheaper answer.
What it costs
Every one of these is paid by something.
- Compositional typing without new names buys you freedom from the combinatorial explosion of declared combinations, and costs discoverability: the resulting type has no name, so it cannot be searched for, documented, or referred to in a review comment.
- Allowing uninhabited intersections buys a simpler, more uniform type algebra — every pair of types has an intersection — and costs the diagnostic: the mistake is reported at the first construction rather than at the declaration that made it impossible.
- Structural intersection buys interoperability with types you do not own, and pays in checking time: every assignability query becomes a per-member walk or a search, which is a measurable share of build time in large codebases.
- Overload sets from intersected call signatures buy an accurate description of "accepts either" and pay with an implementation obligation that no signature states — the type is easy to consume and can be impossible to satisfy.
What else you could do
What a different compiler or language does instead, and when that is better.
- Declare the combined interface explicitly. In Java, C# or Rust this is what you do, and it buys a name, a documentation site, and a fast nominal assignability check — at the cost of updating every implementor when the combination changes.
- Composition over intersection: hold an
Aand aBas two fields instead of claiming one value is both. This is what Go idiom pushes you toward, and it removes the uninhabitability problem entirely because there is no merge. - Traits or type classes (Rust, Haskell, Scala) express "satisfies these capabilities" as a bound rather than as a type, so the capability set is checked at the bound and the value keeps its own type — better whenever you are constraining rather than describing.
- Multiple interface implementation in a nominal language achieves the mixin effect with declared names and virtual dispatch, at the cost of needing a declaration for every combination you actually use.
See it for yourself
The flag, dump or tool that shows you this directly.
- Hover the resulting type in a TypeScript-aware editor; the checker prints the merged shape, and a
neverproperty is visible there long before construction fails. tsc --generateTrace traceDirfollowed by inspecting the trace in a Chrome trace viewer shows which types dominate check time — accumulated intersections in utility types show up as repeatedstructuredTypeRelatedTowork.- Force the checker to tell you whether a type is inhabited:
const _probe: MyIntersection = null as anycompiles for any type, buttype _IsNever<T> = [T] extends [never] ? true : falseevaluated on the alias reports the reduction directly. tsc --noErrorTruncationstops the compiler abbreviating long intersection types in diagnostics, which is the only way to read the error for a deep mixin chain.
Plausible wrong readings
Stated the way a confident engineer states them.
- "
A & Bmeans A or B, since it combines them." It means both at once. The value set shrinks; only the member list grows. - "An impossible intersection is a compiler bug." It is a well-formed type with no inhabitants. Every type system worth using has one, and
[[exhaustiveness-checking]]depends on it. - "Intersection is the same as extending both interfaces." It is structurally similar and nominally nothing: no declaration is created, no
instanceofrelationship exists, and no runtime artifact is emitted. - "If the type compiles, some value has it." Nothing checks inhabitation at the declaration. The first construction attempt is the check, and if nobody ever constructs one, nothing is ever checked.
Misconceptions
The claim, and what is actually true.
{ a: string } & { a: number } is an error.a is never. The type is fine, has a name, and can be passed around. Only constructing it fails.Go deeper
The same idea at increasing depth. Stop wherever it stops being useful.
overview
An intersection type A & B describes a value that is both an A and a B, so you may use everything either one offers. It is the opposite of a union, which describes a value that is one of several and lets you use only what they all share. Intersections are how you say "this object has the library's fields plus mine" without inventing a new name.
practical
Use an intersection when a value genuinely satisfies both members — a mixin result, a request object a middleware has decorated, a generic constraint needing two capabilities. Do not use one to make an error go away: if you cannot describe a value that has the type, you have written an uninhabited type and the failure has merely been deferred to whoever tries to construct it. When an intersection gets deep enough that its hover text does not fit on a screen, give it a name with a type alias — it makes errors readable and stops the checker re-deriving it everywhere.
advanced
Intersection and union are lattice operations — meet and join — over the assignability order, and most of the surprising behaviour follows from that. A & (B | C) distributes to (A & B) | (A & C), which is why an intersection over a union of many members can blow up combinatorially inside a checker. Intersection with never is never because never is the bottom of the lattice; union with unknown is unknown because unknown is the top. The one place the lattice analogy leaks is call signatures: the greatest lower bound of two function types ought to be a function with the union of parameter types, but no such single signature can express "accepts a string, and separately accepts a number, and nothing else", so real checkers keep the overload list instead and quietly stop being a lattice there.
How much this depends on
Nothing in this domain is true of every compiler. These say how much.
A & B there is a genuine greatest lower bound and is commutative, whereas TypeScript's member order matters for overload resolution. Flow reports some uninhabited intersections earlier. Treat the reduction rules as a property of the checker you are using, not of the notation.tsc runs. The magnitude depends entirely on codebase shape; a small project will never notice. Measure with --generateTrace before restructuring anything on this basis.If you were asked this in an interview
- Explain the difference between
A | BandA & Bin terms of value sets and available operations, and say which direction assignability runs in each. - Why does
type T = { id: string } & { id: number }compile, and where does the error eventually appear? - Your build has got slow and the trace blames type checking. What would you look for in a codebase that leans heavily on intersections?