Intersection Types in TypeScript

In TypeScript, Intersection types allow you to merge multiple types into one. Think of it as a "Both/And" relationship. When you intersect two types, the resulting type will possess every single property and method from all the source types. This is a powerful feature for creating highly reusable and modular code without relying strictly on deep class inheritance hierarchies.

Developer Tip: Think of an Intersection as the opposite of a Union. A Union (|) means "it could be A or B," while an Intersection (&) means "it must be A and B at the same time."

 

Key Concepts of Intersection Types

  • Definition: Created using the & (ampersand) operator, an intersection type combines the members of multiple types into a single unit.
  • Use Case: They are perfect for "Mixins" or when you need to combine existing data models, such as adding metadata to a database entity or merging API response parts.
  • Structural Integrity: TypeScript's structural type system ensures that any object assigned to an intersection type satisfies all the combined contracts. If even one property is missing, the compiler will flag it.
Best Practice: Use intersection types to keep your base types small and focused (the Single Responsibility Principle). You can then compose them into more complex types as needed.

 

Example Usage of Intersection Types

Example 1: Basic Intersection Type

Imagine you are building a system that tracks employees. Every employee is a person, but they also have professional attributes.

type Person = {
  name: string;
  age: number;
};

type Employee = {
  role: string;
  salary: number;
};

// Combining Person and Employee into one type
type EmployeeDetails = Person & Employee;

let employee: EmployeeDetails = {
  name: "John",
  age: 30,
  role: "Senior Developer",
  salary: 95000,
};

In this example, the EmployeeDetails type is the "sum" of Person and Employee. If you were to omit the salary property, TypeScript would throw an error because EmployeeDetails strictly requires everything from both definitions.

Common Mistake: Forgetting that intersections are additive. Beginners sometimes assume that & acts like a logical "AND" in a way that restricts properties, but it actually accumulates them.

Example 2: Intersection with Multiple Types

You can intersect as many types as you need. This is common when building configuration objects or complex data structures.

type Address = {
  street: string;
  city: string;
};

type ContactInfo = {
  email: string;
  phone: string;
};

type SocialMedia = {
  linkedIn?: string;
  twitter?: string;
};

type FullContact = Address & ContactInfo & SocialMedia;

let contact: FullContact = {
  street: "123 Tech Lane",
  city: "San Francisco",
  email: "[email protected]",
  phone: "555-0199",
  linkedIn: "linkedin.com/in/devprofile"
};

Here, FullContact aggregates three different type definitions. This modular approach allows you to reuse Address or ContactInfo in other parts of your application independently.

Example 3: Intersection with Interfaces

While type aliases are commonly used with intersections, you can also intersect interfaces. This is useful when you want to combine third-party library interfaces with your own local requirements.

interface Product {
  id: number;
  name: string;
}

interface Price {
  price: number;
  currency: string;
}

// Intersecting two interfaces to create a new type
type MarketableProduct = Product & Price;

let laptop: MarketableProduct = {
  id: 101,
  name: "MacBook Pro",
  price: 2400,
  currency: "USD"
};
Watch Out: If two types have a property with the same name but different, incompatible types (e.g., id: string vs id: number), the intersection will result in never for that property, making the object impossible to instantiate.

 

Combining Classes with Intersection Types

Because TypeScript uses structural typing, an object can satisfy an intersection of classes even if it isn't an actual instance of those classes. It just needs to "look" like them by having the correct properties and methods.

class Car {
  make: string = "";
  model: string = "";
  drive() {
    console.log("The vehicle is moving.");
  }
}

class Electric {
  batteryLevel: number = 100;
  charge() {
    console.log("Charging battery...");
  }
}

// This type requires everything from both Car and Electric
type ElectricCar = Car & Electric;

const myTesla: ElectricCar = {
  make: "Tesla",
  model: "Model 3",
  batteryLevel: 85,
  drive() { console.log("Silent driving..."); },
  charge() { console.log("Plugged into Supercharger."); }
};

In this scenario, ElectricCar acts as a blueprint that enforces the presence of both "Car-like" and "Electric-like" behavior. This is often more flexible than using traditional class inheritance, as it avoids the "diamond problem" of multiple inheritance.

 

Using Intersection Types with Functions

Intersections can also be applied to function signatures. This is typically used to describe Function Overloading, where a single function might be able to handle multiple sets of parameters.

type Greet = (name: string) => string;
type Log = (message: string) => void;

// Combined, this function must be able to act as both (in specific contexts)
type LoggerGreeting = Greet & Log;

// Note: In practice, function intersections are mostly used for 
// sophisticated library definitions and advanced type narrowing.
Developer Tip: Function intersections are a bit of an advanced edge case. Most of the time, you'll use standard function overloading syntax, but knowing this exists helps when reading complex TypeScript definition files (.d.ts).

 

Type Narrowing with Intersection Types

When you have an intersection type, you don't usually need to "narrow" the type to access properties because the object is guaranteed to have all of them. However, intersections are frequently used in Type Guards to safely merge properties during runtime checks.

type Admin = { role: string; permissions: string[] };
type User = { username: string; email: string };

type AdminUser = Admin & User;

function manageAccount(user: AdminUser) {
  // We can safely access both Admin and User properties immediately
  console.log(`Checking permissions for ${user.username}...`);
  if (user.permissions.includes("admin_panel")) {
    console.log(`Access granted to ${user.role} role.`);
  }
}

const activeAdmin: AdminUser = {
  username: "root_access",
  email: "[email protected]",
  role: "SuperAdmin",
  permissions: ["read", "write", "admin_panel"],
};

manageAccount(activeAdmin);

By using AdminUser, you eliminate the need to constantly check if the role property exists on a standard User. The intersection makes the requirement explicit.

 

Summary

  • The "And" Logic: Intersection types combine multiple definitions into one, requiring an object to satisfy all requirements simultaneously.
  • Operator: Uses the & symbol between types, interfaces, or classes.
  • Code Reuse: They facilitate a "composition-over-inheritance" design pattern, making your codebase more modular and easier to maintain.
  • Strictness: TypeScript will prevent you from creating objects that are missing any piece of the combined intersection, ensuring high runtime reliability.