# TypeScript Union Types vs Intersection Types: How the Compiler Handles Type Composition

> Understand TypeScript union and intersection types. Learn how they combine types and how the compiler handles differences to improve your code.

- Repository: [Microsoft/TypeScript](https://github.com/microsoft/typescript)
- Tags: deep-dive
- Published: 2026-02-13

---

**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`](https://github.com/microsoft/TypeScript/blob/main/src/compiler/types.ts) at approximately line 6745, the `UnionType` interface extends `UnionOrIntersectionType`:

```typescript
interface UnionType extends UnionOrIntersectionType {
    // Union-specific properties
}

```

The compiler creates union types through the `getUnionType` method in [`src/compiler/checker.ts`](https://github.com/microsoft/TypeScript/blob/main/src/compiler/checker.ts), which performs type reduction and normalization to eliminate redundant constituents.

### Union Type Example

```typescript
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`](https://github.com/microsoft/TypeScript/blob/main/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`](https://github.com/microsoft/TypeScript/blob/main/src/compiler/checker.ts) handles the creation of these types, performing normalization to resolve overlapping properties and handle conflicting type constraints.

### Intersection Type Example

```typescript
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`](https://github.com/microsoft/TypeScript/blob/main/src/compiler/types.ts) | `IntersectionType` in [`src/compiler/types.ts`](https://github.com/microsoft/TypeScript/blob/main/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**: `UnionType` interface
- **Line ~6756**: `IntersectionType` interface

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`](https://github.com/microsoft/TypeScript/blob/main/src/compiler/checker.ts) exposes the primary methods for constructing these types:

- `getUnionType(types: Type[], reduction?: TypeFlags): UnionType`
- `getIntersectionType(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`](https://github.com/microsoft/TypeScript/blob/main/src/services/types.ts) at approximately line 126:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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 the `UnionType` interface in [`src/compiler/types.ts`](https://github.com/microsoft/TypeScript/blob/main/src/compiler/types.ts) at line ~6745.
- **TypeScript intersection types** (`A & B`) require values to satisfy all constituent types simultaneously, implemented as the `IntersectionType` interface at line ~6756.
- The compiler creates these types via `getUnionType()` and `getIntersectionType()` in [`src/compiler/checker.ts`](https://github.com/microsoft/TypeScript/blob/main/src/compiler/checker.ts), with type guards `isUnion()` and `isIntersection()` available in [`src/services/types.ts`](https://github.com/microsoft/TypeScript/blob/main/src/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`](https://github.com/microsoft/TypeScript/blob/main/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.