---
title: "Combining Types and Interfaces with & and | in TypeScript"
date: "2023-09-21"
slug: "combining-types-and-interfaces-with-or-in-typescript"
author: "Dany Paredes"
canonical: "https://danywalls.com/combining-types-and-interfaces-with-or-in-typescript"
description: "Learn how to compose TypeScript types using Union (|), Intersection (&), and interface extends with practical examples, decision matrices, and real-world patterns."
---


When modeling application data in TypeScript, you quickly reach a point where basic types are not enough. You need to compose types together: combining user data with permissions, handling API responses that return either success data or an error payload, or extending existing third-party interfaces.

TypeScript gives us three primary tools to achieve this:
1. **Union Types (`|`)**
2. **Intersection Types (`&`)**
3. **Interface Inheritance (`extends`)**

In this guide, we will explore how each operator works, when to use them in real-world applications, and how to avoid common type composition pitfalls.

Let's start by looking at our baseline domain models.

---

## 1. The Baseline Interfaces 🏀

Imagine we are building a sports management dashboard. We have two separate interfaces that describe different domain aspects:

```typescript
interface Player {
  id: number;
  name: string;
}

interface PlayerStats {
  points: number;
  position: string;
}
```

- `Player` handles identity information.
- `PlayerStats` handles match performance metrics.

How do we combine them when a feature requires both or either of these structures? Let's explore Union Types first.

---

## 2. Union Types (`|`): Handling Multiple Possibilities 🔀

A **Union Type** (using the `|` operator) tells TypeScript: *"This value can be of Type A **OR** Type B."*

```typescript
export type PlayerOrStats = Player | PlayerStats;
```

A variable of type `PlayerOrStats` can be:
1. An object satisfying `Player`
2. An object satisfying `PlayerStats`
3. An object satisfying **both**

### Practical Examples

```typescript
// ✅ 1. Valid: Only Player properties
const playerOnly: PlayerOrStats = {
  id: 1,
  name: "Diana Taurasi"
};

// ✅ 2. Valid: Only PlayerStats properties
const statsOnly: PlayerOrStats = {
  points: 24,
  position: "Guard"
};

// ✅ 3. Valid: Both sets of properties
const fullPlayer: PlayerOrStats = {
  id: 2,
  name: "Breanna Stewart",
  points: 30,
  position: "Forward"
};
```

### Accessing Properties in Union Types

When accessing properties on a union type, TypeScript only allows accessing properties that are **common to all union members** unless you narrow the type:

```typescript
function printPlayerInfo(data: PlayerOrStats) {
  // ❌ Error: Property 'name' does not exist on type 'PlayerStats'
  // console.log(data.name);

  // ✅ Solution: Narrow with the 'in' operator
  if ("name" in data) {
    console.log(`Player Name: ${data.name}`);
  }
}
```

> If you want to learn all the ways to narrow union types safely at runtime, check out my deep-dive on [how to check types in TypeScript](/how-to-check-types-in-typescript).

Now that we understand unions, let's see what happens when we need an object that strictly requires all properties.

---

## 3. Intersection Types (`&`): Merging Multiple Types 🔗

An **Intersection Type** (using the `&` operator) combines multiple types into one. It tells TypeScript: *"This value MUST satisfy Type A **AND** Type B."*

```typescript
export type CompletePlayer = Player & PlayerStats;
```

With `CompletePlayer`, any assigned object must include all properties from both `Player` and `PlayerStats`:

```typescript
// ❌ Error: Property 'points' and 'position' are missing
const incompletePlayer: CompletePlayer = {
  id: 1,
  name: "A'ja Wilson"
};

// ✅ Valid: Contains every required property from both interfaces
const validPlayer: CompletePlayer = {
  id: 1,
  name: "A'ja Wilson",
  points: 28,
  position: "Center"
};
```

### Intersection Gotcha: Incompatible Property Types (`never`)

Be cautious when intersecting types that declare the same property name with conflicting primitive types:

```typescript
interface ServerResponseA {
  status: string;
}

interface ServerResponseB {
  status: number;
}

// ⚠️ 'status' becomes (string & number), which evaluates to 'never'
type MergedResponse = ServerResponseA & ServerResponseB;
```

Because a property cannot simultaneously be a `string` and a `number`, the `status` field becomes `never`, making `MergedResponse` impossible to instantiate without a type assertion.

Now let's compare intersection types with interface inheritance.

---

## 4. `interface extends` vs Intersection (`&`) 🏛️

TypeScript interfaces provide an alternative syntax for composition: `extends`.

```typescript
interface ExtendedPlayer extends Player {
  points: number;
  position: string;
}
```

Both `CompletePlayer (Player & PlayerStats)` and `ExtendedPlayer` require the same final shape. Here is how they compare in practice:

| Feature | Interface Inheritance (`extends`) | Intersection Types (`&`) |
| :--- | :--- | :--- |
| **Primary Use Case** | Hierarchical object contracts and domain entities | Ad-hoc composition, utility types, and unions |
| **Type Flexibility** | Can only extend object shapes and statically known types | Can compose any types (primitives, unions, generics) |
| **Compiler Performance** | Faster caching by the TypeScript compiler | Computed dynamically on every evaluation |
| **Conflict Detection** | Emits a compiler error if property types mismatch | Merges conflicting primitives into `never` |
| **Declaration Merging** | Supported (interfaces with same name auto-merge) | Not supported on type aliases |

### When to use `extends`:
- When designing your core domain entities and object hierarchies.
- When you want the TypeScript compiler to alert you immediately if a subtype attempts to override a parent property with an incompatible type.

### When to use `&` (Intersection):
- When combining a type alias with an interface.
- When creating utility types or merging generic constraints in functions.
- When combining union members dynamically.

Next, let's look at one of the most powerful real-world patterns: Discriminated Unions.

---

## 5. Real-World Pattern: Discriminated Unions 🛡️

The most common architectural pattern combining unions and intersections in production is the **Discriminated Union** for asynchronous API states:

```typescript
type LoadingState = { status: "loading" };
type SuccessState<T> = { status: "success"; data: T };
type ErrorState = { status: "error"; message: string };

type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState;

function handleState(state: AsyncState<Player[]>) {
  switch (state.status) {
    case "loading":
      return "Loading players...";
    case "success":
      return `Loaded ${state.data.length} players.`;
    case "error":
      return `Error: ${state.message}`;
  }
}
```

Because each type has a unique literal property (`status`), TypeScript automatically narrows the object inside each `case` branch with complete type safety and autocompletion.

---

## Recap 🛠️

Here is a quick summary to guide your daily coding decisions:

1. **Union (`|`)**: Use when a value can be one of several possible types (e.g. `string | number`, API request variants).
2. **Intersection (`&`)**: Use when you need to merge existing types, utility types, or generics into a single composite type.
3. **`interface extends`**: Use for standard object-oriented hierarchies and clear domain entities.
4. **Discriminated Unions**: Combine literal discriminator tags with unions for robust state machines and API response handling.

For more hands-on TypeScript techniques, check out my articles on [How to Check Types in TypeScript](/how-to-check-types-in-typescript), [3 Ways of Type Transformation in TypeScript](/3-ways-of-type-transformation-in-typescript), and [Why You Should Avoid Using 'any' in TypeScript](/why-avoid-using-any-in-typescript)!

