TypeScript Intersection Types

May 18, 2023#typescript

TypeScript intersection types are a way of creating a new type that combines multiple existing types. The new type has all the features of the combined types, and useful when you want to compose or combine types instead of creating them from scratch.

You can create an intersection type by using the ampersand (&) symbol between two or more types. For example, A & B means a type that has all the properties of both A and B. You can use intersection types to mix different types together:

interface Employee {
  id: number;
  name: string;
  department: string;
}

interface Manager {
  teamSize: number;
  projects: string[];
}

type ManagerEmployee = Employee & Manager;

const manager: ManagerEmployee = {
  id: 1,
  name: "John Doe",
  department: "Engineering",
  teamSize: 5,
  projects: ["Project A", "Project B"],
};

Intersection types can also be used to extend existing types by adding new properties or making some properties optional. For example, you can use an intersection type to create a new type that represents a Employee with an optional address property:

type NewEmployee = Employee & { address?: string };

let employee: NewEmployee = {
  id: 2,
  name: "John Smith",
  department: "Marketing",
  address: "CA"
};

Invalid intersection

Intersection types are useful when you want to combine multiple types into one and access all their properties. However, they are not always compatible with each other. TypeScript will only allow intersection types that make sense and do not conflict with each other. For example, you cannot create an intersection type of a number and a string:

type NumberString = number & string; 
let a: NumberString = 12; // error: Type 'number' is not assignable to type 'never'.

The TypeScript compiler does not raise an error when you declare a type that is an intersection of number and string because it is a valid type expression. However, it does assign the type never to any variable or value that has this type because there is no possible value that can have this type.

The type never is a special type in TypeScript that represents the type of values that never occur. For example, the return type of a function that always throws an exception or never returns is never. The type never is also used to represent the empty set of values, which is the case for the intersection of number and string.

Between interface and class

You can use intersection between an interface and a class. However, you need to be careful about the differences between types and values in TypeScript.

An interface is a type that describes the shape of an object. A class is both a type and a value. The type describes the shape of the instances of the class, while the value is a constructor function that creates new instances.

When you use an intersection between an interface and a class, you are creating a new type that has all the properties of both the interface and the class. However, you are not creating a new value or a new constructor function. You still need to use the existing constructor function of the class to create new instances of the intersection type.

// Define an interface for Employee
interface Employee {
  name: string;
  salary: number;
}

// Define a class for Manager
class Manager {
  department: string;

  constructor(department: string) {
    this.department = department;
  }

  assignTask(task: string) {
    console.log(`Assigning task: ${task}`);
  }
}

// Create an intersection type for EmployeeManager
type EmployeeManager = Employee & Manager;

// Create an instance of EmployeeManager using the Manager constructor
let m: Manager = new Manager("Sales");
let em: EmployeeManager = {
  ...m,
  name: "Alice",
  salary: 5000,
  assignTask(task: string) {
    console.log(`Assigning task: ${task}`);
  }
};

// Try to create an instance of EmployeeManager using the EmployeeManager constructor
let em2: EmployeeManager = new EmployeeManager("Marketing"); // Error: 'EmployeeManager' only refers to a type, but is being used as a value here.

Difference from union types

The difference between intersection types and union types is that intersection types combine multiple types into one, while union types describe a value that can be one of several types.

For example, if you have two interfaces A and B, an intersection type A & B has all the properties of both A and B, while a union type A | B can be either A or B, but not both.

interface Employee {
  id: number;
  name: string;
  department: string;
}

interface Manager {
  teamSize: number;
  projects: string[];
}

type EmployeeOrManager = Employee | Manager;

function printDetails(person: EmployeeOrManager) {
  console.log(person.id); // Accessing id property (common to both Employee and Manager)
  console.log(person.name); // Accessing name property (common to both Employee and Manager)
  
  if ("department" in person) {
    console.log("Department:", person.department); // Accessing department property (specific to Employee)
  }
  
  if ("teamSize" in person) {
    console.log("Team Size:", person.teamSize); // Accessing teamSize property (specific to Manager)
    console.log("Projects:", person.projects); // Accessing projects property (specific to Manager)
  }
}

Union types provide flexibility by allowing values of multiple types, but they also impose a level of uncertainty as you may need to perform type checks or type assertions to narrow down the specific type of a value.