Unions and Intersections
Combining types with OR and AND to model flexible APIs and complex data shapes
So far, we've built types one piece at a time: primitives, objects,
arrays, functions. But real-world APIs often need to express "this or
that" (a value can be one of several types) or "this and that" (a value
must satisfy multiple constraints at once). TypeScript provides union
types (A | B) and intersection types (A & B) to handle these
cases. They're the set-theoretic building blocks for expressing complex,
precise types.
This chapter covers: union types, intersection types,
set-theoretic intuition, when intersections produce never, and
designing APIs with unions.
Union types: "this or that"
A union type A | B represents a value that is either type A
or type B (or both, if they overlap):
What the compiler now knows: value can be a string or a number,
but nothing else. If you try to assign a boolean, the compiler rejects
it.
Unions shine when a function can accept multiple input types:
This is far more precise than using any (which disables all type
checking) or creating separate overloads for string and number.
Working with union types: narrowing required
When you have a union, the compiler doesn't know which member it is at any given moment. So you can only call methods or access properties that exist on all members:
The line value.length fails because number doesn't have a length
property. To access length, you must narrow the type (we'll cover
narrowing in detail in Type Narrowing):
The typeof check tells the compiler "in this branch, value is a
string." The compiler tracks this and allows value.length.
Intersection types: "this and that"
An intersection type A & B represents a value that is both type
A and type B simultaneously:
The type Person has both the name property from Named and the
age property from Aged. Intersections merge object types.
Intersections are useful for mixins — combining multiple interfaces or types into one:
The resulting type has all the properties from all three constituents.
Set-theoretic intuition: unions widen, intersections narrow
Think of types as sets of values. A union A | B is the union of the
two sets (all values that are in A, or in B, or in both). An intersection
A & B is the intersection of the two sets (only values that are in
both A and B).
Unions widen the set of possible values: string | number accepts
more values than just string or just number. Intersections narrow
the set: Named & Aged accepts only values that satisfy both
constraints.
For primitive types, intersections can produce an empty set:
string & number is never (the bottom type) because no value can be
both a string and a number. This is a compile-time error — the compiler
knows the type is uninhabited.
Discriminated unions: tagged unions for type-safe variants
A discriminated union (also called a "tagged union") is a union of object types, each with a discriminator field (a literal type) that uniquely identifies the variant:
The field kind is the discriminator. By checking shape.kind, the
compiler narrows shape to the correct variant. This is incredibly
powerful for modeling state machines, API responses, and other variant
data.
What bugs does this prevent? Without the discriminator, you'd have to
manually check which properties exist (e.g., "radius" in shape). With
discriminated unions, the compiler does it for you, and the narrowing is
exhaustive — if you add a new variant, the compiler will tell you
everywhere you need to handle it.
Exhaustiveness checking with discriminated unions
When you handle all cases of a discriminated union, the compiler can verify that you haven't missed any. This is called exhaustiveness checking:
If you add a fourth variant (say, Timeout) to Result, the compiler
will complain in handleResult — the else branch no longer handles all
cases. This forces you to update the code.
A common idiom for exhaustiveness checking is to use a never-typed
variable in the final else:
If status is not fully handled, the line const _exhaustive: never = status will error, because status won't be never (it will be the
unhandled variant).
Union of function types
You can also have unions of function types, though this is less common:
When calling a Func, the compiler doesn't know which variant it is, so
you can only call it with arguments that satisfy all overloads (in this
case, none exist, so it's tricky to use). Function unions are often
better expressed with overloads or generics.
Intersection of object types: merging properties
When you intersect two object types, the result has all properties from both:
This is equivalent to declaring an interface that extends both:
Both approaches are valid; choose based on style and whether you need
type features (like unions).
When intersections conflict: never
If you intersect two types with conflicting properties, the result is
never:
The property value must be both string and number, which is
impossible. So Conflict is effectively never. The compiler will catch
this at declaration time in most cases.
Designing APIs with unions: flexibility and safety
Unions let you model flexible APIs without sacrificing type safety. For example, a function that accepts a configuration object or a simple string:
This is more ergonomic than forcing callers to always provide a full
Config object, but still type-safe — you can't pass a boolean or an
array.
Practice: a result type with discriminated union
Let's solidify these concepts with a challenge. You'll define a Result
type for error handling (similar to Rust's Result<T, E>), then
implement functions that construct and consume it.
Define a discriminated union Result<T> = { ok: true; value: T } | { ok: false; error: string }. Implement two functions: success<T>(value: T): Result<T> (constructs a success result) and unwrap<T>(result: Result<T>): T (returns the value if ok, throws an error otherwise). Test with unwrap(success(42)) (should return 42) and unwrap({ ok: false, error: "fail" }) (should throw).
Check your understanding
What does the union type string | number represent?
A value that is both a string and a number
A value that is neither a string nor a number
A value that is either a string or a number
A value that is only a string
What does the intersection type A & B represent for two object types A and B?
A value that is either A or B
A value that has all properties from both A and B
A value that has no properties
A value that is compatible with neither A nor B
Summary
Union types (A | B) let you express "this or that," widening the set
of acceptable values. Intersection types (A & B) let you express "this
and that," narrowing the set to values that satisfy both constraints.
Discriminated unions (tagged unions) use literal types as discriminators
to enable type-safe variant handling with exhaustiveness checking.
Unions are essential for flexible APIs: a function can accept multiple input types without losing precision. Intersections are essential for mixins and combining constraints. Together, they're the building blocks for complex, expressive type systems.
In the next chapter, Type Narrowing, we'll see how
the compiler tracks control flow to refine union types — how typeof,
instanceof, in, and custom type guards let you safely work with
values of union types.