TypeScript Union Types vs Intersection Types: How the Compiler Handles Type Composition
TypeScript union types (A | B) allow a value to be one of several types, while intersection types (A & B) require a value to satisfy all types simultaneously.
In the microsoft/TypeScript compiler, these two type composition mechanisms form the backbone of flexible type systems. Union types represent "either/or" scenarios, while intersection types create "and" relationships that merge type capabilities. Both are implemented as distinct interfaces extending a common base class within the compiler's type system.
What Are TypeScript Union Types?
A union type describes a value that can be one of several specified types. The compiler represents this internally using the UnionType interface, which stores the constituent types in a types array.
In src/compiler/types.ts at approximately line 6745, the UnionType interface extends UnionOrIntersectionType:
interface UnionType extends UnionOrIntersectionType {
// Union-specific properties
}
The compiler creates union types through the getUnionType method in src/compiler/checker.ts, which performs type reduction and normalization to eliminate redundant constituents.
Union Type Example
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; sideLength: number };
function area(s: Shape): number {
// Type narrowing based on discriminant property
if (s.kind === "circle") {
return Math.PI * s.radius ** 2; // s is narrowed to circle
}
return s.sideLength ** 2; // s is narrowed to square
}
What Are TypeScript Intersection Types?
An intersection type combines multiple types into one, requiring a value to satisfy all constituent types simultaneously. The compiler implements this via the IntersectionType interface at approximately line 6756 in src/compiler/types.ts.
Like unions, intersections extend UnionOrIntersectionType and store member types in a types array, but the semantic checking requires values to match every constituent rather than any single one.
The getIntersectionType function in src/compiler/checker.ts handles the creation of these types, performing normalization to resolve overlapping properties and handle conflicting type constraints.
Intersection Type Example
type Printable = { print(): void };
type Serializable = { serialize(): string };
type PrintableSerializable = Printable & Serializable;
class Document implements PrintableSerializable {
print() {
console.log("Printing document...");
}
serialize() {
return JSON.stringify(this);
}
}
Key Differences Between Union and Intersection Types
The fundamental distinction lies in the logical operators they represent: union types use OR logic (any one type), while intersection types use AND logic (all types).
| Aspect | Union Types (A | B) |
Intersection Types (A & B) |
|---|---|---|
| Value Requirement | Must satisfy at least one constituent type | Must satisfy all constituent types simultaneously |
| Internal Interface | UnionType in src/compiler/types.ts |
IntersectionType in src/compiler/types.ts |
| Creation Method | checker.getUnionType(types) |
checker.getIntersectionType(types) |
| Type Narrowing | Supported via control-flow analysis and type guards | Not applicable (already requires all properties) |
| Common Use Cases | Function overloads, discriminated unions, optional values | Mixins, object composition, merging interfaces |
How the TypeScript Compiler Implements These Types
Core Type Definitions in src/compiler/types.ts
Both union and intersection types share a common base interface, UnionOrIntersectionType, defined in the compiler's type system definitions. This base contains the types: Type[] property that holds the constituent type references.
The specific interfaces are located at:
- Line ~6745:
UnionTypeinterface - Line ~6756:
IntersectionTypeinterface
Additionally, the AST node representations are defined as:
- Line ~2300:
UnionTypeNode - Line ~2305:
IntersectionTypeNode
Type Checker APIs
The TypeChecker class in src/compiler/checker.ts exposes the primary methods for constructing these types:
getUnionType(types: Type[], reduction?: TypeFlags): UnionTypegetIntersectionType(types: Type[], reduction?: TypeFlags): IntersectionType
These methods handle type normalization, eliminating duplicate types in unions and resolving property conflicts in intersections.
Type Guard Utilities
For compiler services and language features, type guards are exported from src/services/types.ts at approximately line 126:
function isUnion(): this is UnionType;
function isIntersection(): this is IntersectionType;
These allow the compiler to quickly discriminate between union and intersection representations during type analysis and code generation.
Practical Examples
Discriminated Unions with Union Types
Discriminated unions represent one of the most powerful patterns enabled by union types, allowing exhaustive type checking:
type NetworkState =
| { state: "loading" }
| { state: "failed"; code: number }
| { state: "success"; response: string };
function handleState(state: NetworkState) {
switch (state.state) {
case "loading":
return "Loading...";
case "failed":
return `Error ${state.code}`; // TypeScript knows code exists here
case "success":
return state.response; // TypeScript knows response exists here
}
}
Mixin Patterns with Intersection Types
Intersection types enable the mixin pattern, combining multiple class capabilities:
type Constructor<T = {}> = new (...args: any[]) => T;
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
timestamp = Date.now();
};
}
function Activatable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
isActive = true;
toggle() {
this.isActive = !this.isActive;
}
};
}
// Creating an intersection of capabilities
const TimestampedActivatableUser = Timestamped(Activatable(class {
name = "user";
}));
const instance = new TimestampedActivatableUser();
// instance has name, isActive, toggle(), and timestamp
Using the Compiler API Internally
When working with the TypeScript compiler API directly, you manipulate these types programmatically:
import * as ts from "typescript";
const program = ts.createProgram(["file.ts"], {});
const checker = program.getTypeChecker();
// Creating types programmatically (simplified example)
const stringType = checker.getStringType();
const numberType = checker.getNumberType();
// Union: string | number
const unionType = checker.getUnionType([stringType, numberType]);
// Intersection: string & number (effectively never in practice)
const intersectionType = checker.getIntersectionType([stringType, numberType]);
Summary
- TypeScript union types (
A | B) allow values to be one of several types, implemented internally as theUnionTypeinterface insrc/compiler/types.tsat line ~6745. - TypeScript intersection types (
A & B) require values to satisfy all constituent types simultaneously, implemented as theIntersectionTypeinterface at line ~6756. - The compiler creates these types via
getUnionType()andgetIntersectionType()insrc/compiler/checker.ts, with type guardsisUnion()andisIntersection()available insrc/services/types.ts. - Union types enable discriminated unions and type narrowing, while intersection types support mixins and object composition patterns.
Frequently Asked Questions
When should I use union types versus intersection types?
Use union types when a value can be one of several distinct alternatives, such as different states in a state machine ("loading" | "success" | "error") or different shapes of data. Use intersection types when you need to combine multiple type capabilities into one, such as merging interfaces (Serializable & Printable) or creating mixins where a class must implement multiple behaviors simultaneously.
Can a type be both a union and an intersection?
Yes, types can nest these constructs arbitrarily. You can have a union of intersections ((A & B) | (C & D)) or an intersection containing unions ((A | B) & (C | D)). The TypeScript compiler handles these recursively using the same UnionType and IntersectionType internal representations, flattening and normalizing them according to the type system's rules.
How does TypeScript handle empty unions or intersections?
An empty union (never in TypeScript terminology) represents a type with no possible values, while an empty intersection effectively reduces to the universal type unknown or {} depending on context. The compiler's getUnionType and getIntersectionType methods in src/compiler/checker.ts handle these edge cases by returning the appropriate bottom or top types, ensuring the type system remains sound even with degenerate cases.
Do union and intersection types affect JavaScript runtime performance?
No, union and intersection types are compile-time only constructs. They are erased during the TypeScript-to-JavaScript compilation process and leave no trace in the generated code. The runtime performance characteristics depend entirely on the underlying JavaScript values and operations, not on the TypeScript type annotations. The compiler uses these types solely for static analysis, type checking, and IntelliSense generation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s https://instagit.com/install.md