Intersection Types in TypeScript: Tutorial with Examples

Updated: January 8, 2024 By: Guest Contributor Post a comment

Introduction

TypeScript offers robust typing capabilities that enhance JavaScript development. Intersection types are a powerful feature within TypeScript’s type system that allow for the combination of multiple types into one. This tutorial explores the concept of intersection types and provides practical examples to illustrate their utility in TypeScript programming.

Basic Intersection Types

Intersection types enable you to combine multiple types into one. This is particularly useful when you want to create a type that has all the properties of the combined types.

interface Runnable {
   run(): void;
 }

interface Drivable {
drive(): void;
}

// Intersection Type
// It can run AND drive
type Actionable = Runnable & Drivable;

In this example, Actionable is an intersection type that has both run and drive methods.

Advanced Usage of Intersection Types

Intersection types aren’t limited to combining interfaces. They can also be used with other types including unions, primitives, and tuples.

type ID = string | number;
 interface User {
   name: string;
 }</n type UserWithID = User & { id: ID }; 

Here, UserWithID is an intersection type that includes a user’s name and an id that can be either a string or a number.

Combining Interfaces with Intersection Types

Now let’s use intersection types to extend interfaces with additional properties or methods. This helps to modularize and reuse code.

interface Person {
   name: string;
 }
 interface Loggable {
   log(): void;
 }

// A type that is both a Person and Loggable
type LoggablePerson = Person & Loggable;

let user: LoggablePerson = {
  name: 'Alice',
  log: () => console.log('User:', user.name)
};
user.log(); // Output: User: Alice

In the preceding code snippet, LoggablePerson has both the name property from Person and the log method from Loggable.

Intersection Types with Functions

Intersection types can be especially handy when working with high-order functions or when specifying a function that must meet several type constraints.

type StringConvertor = {
   (val: string): string;
 };
 type NumberConvertor = {
   (val: number): string;
 };

// A type that can convert both strings and numbers to strings
// Notice the use of function overloading like approach
let convert: StringConvertor & NumberConvertor = (val: string | number): string => {
  return val.toString();
};

console.log(convert(‘Hello’)); // Output: Hello
console.log(convert(123)); // Output: 123

This function can now handle both strings and numbers, returning a string in each case due to the combined type StringConvertor & NumberConvertor.

Applications within Generics

Intersection types can also be integrated with generics to create very flexible and reusable components. Here’s an example using generics with intersection types:

function extend<T,U>(first: T, second: U): T & U {
   const result: Partial = {};
   for (let id in first) {
       (result as T & U)[id] = first[id];
   }
   for (let id in second) {
       if (!result.hasOwnProperty(id)) {
            (result as T & U)[id] = second[id];
       }
   }
   return result as T & U;
 }

const x = extend({ a: 'hello' }, { b: 42 });
console.log(x.a); // Output: hello
console.log(x.b); // Output: 42

In the example above, the extend function merges two objects into one, creating an intersection of the generics T and U.

Conclusion

Intersection types in TypeScript provide an elegant way to combine multiple types, offering flexibility and robustness in type composition. As seen in this tutorial, intersection types can be applied to a wide range of scenarios, from extending interfaces to creating high-order functions and integrating with generics. Mastering intersection types can significantly improve the quality and maintainability of your TypeScript code.