Altcademy - a Forbes magazine logo Best Coding Bootcamp 2023

What are Generics in TypeScript?

In this blog post, we're going to tackle the concept of Generics in TypeScript. If you're new to programming or have only a little experience, don't worry! We're going to walk through this topic step by step, explaining any jargons, and providing lots of examples along the way.

What are Generics?

Generics are a feature of TypeScript that allows you to write reusable, flexible, and type-safe code. The main idea behind generics is to create functions, classes, and interfaces that can work with a variety of data types, without losing the benefits of type checking. In simpler terms, generics are like templates for writing code that can handle different types of data.

To better understand the concept of generics, let's use a real-life example. Imagine you have a box, and you want to use this box to store different items, like books, toys, or even fruits. Now, you could create separate boxes for each type of item, but it would be much more efficient to have a single box that can store any item, right? Generics in TypeScript work in a similar way. They allow you to create a "box" (i.e., a function, class, or interface) that can handle any data type.

Why do we need Generics?

Before we dive into the details of how to use generics, let's discuss why they're important in the first place. One of the main benefits of TypeScript is its strong typing system, which helps you catch errors during development, rather than at runtime. This can save a lot of time and potential headaches.

However, sometimes you need to write code that can work with multiple data types. Without generics, you would have to create separate functions or classes for each data type, which can be tedious and lead to code duplication. Generics help you avoid this problem by allowing you to write one piece of code that can handle multiple data types while maintaining type safety.

Let's look at a simple example to illustrate this point. Imagine you need to write a function that takes an array and returns the first element. Without generics, you might write something like this:

function getFirstElement(numbers: number[]): number {
  return numbers[0];
}

function getFirstString(strings: string[]): string {
  return strings[0];
}

As you can see, we have two separate functions, one for arrays of numbers and one for arrays of strings. This works, but it's not very efficient, especially if you need to handle more data types in the future. With generics, you can write a single function that works with any array:

function getFirstElement<T>(elements: T[]): T {
  return elements[0];
}

This new function uses a generic type parameter, T, which acts as a placeholder for the actual data type. Now we can use the same function for both numbers and strings (or any other data type):

const numbers = [1, 2, 3];
const strings = ["a", "b", "c"];

console.log(getFirstElement(numbers)); // 1
console.log(getFirstElement(strings)); // "a"

As you can see, generics help us write more reusable and flexible code, without sacrificing type safety.

How to use Generics

Now that we understand what generics are and why they're useful, let's dive into how to use them in TypeScript. There are three main ways to use generics: with functions, classes, and interfaces. We'll cover each of these in detail below.

Generics with Functions

We've already seen a simple example of using generics with functions. To recap, here's the generic version of the getFirstElement function:

function getFirstElement<T>(elements: T[]): T {
  return elements[0];
}

The syntax for using generics in a function is to add angle brackets (<>) after the function name, followed by a type parameter (usually a single uppercase letter, like T). This type parameter can then be used within the function signature and body, as a placeholder for the actual data type.

You can also use multiple generic type parameters if needed. For example, consider a function that takes two arguments and returns a tuple (i.e., an array with a fixed number of elements and specific types) containing the arguments:

function createTuple<A, B>(a: A, b: B): [A, B] {
  return [a, b];
}

const tuple = createTuple(1, "a"); // [1, "a"]

In this example, we're using two generic type parameters, A and B, to allow the function to accept two arguments of different types and return a tuple with the matching types.

Generics with Classes

Generics can also be used with classes to create more flexible and reusable data structures. Let's look at an example of a simple stack data structure:

class Stack<T> {
  private elements: T[] = [];

  push(element: T): void {
    this.elements.push(element);
  }

  pop(): T | undefined {
    return this.elements.pop();
  }

  peek(): T | undefined {
    return this.elements[this.elements.length - 1];
  }
}

In this example, we're using the generic type parameter T to define the type of elements that the stack can store. This allows us to create stacks for any data type, while still maintaining type safety:

const numberStack = new Stack<number>();
numberStack.push(1);
console.log(numberStack.peek()); // 1

const stringStack = new Stack<string>();
stringStack.push("a");
console.log(stringStack.peek()); // "a"

Notice that we use the generic type parameter in the class definition (class Stack<T>) and within the class methods to specify the type of the elements array and the return values of the pop and peek methods.

Generics with Interfaces

Finally, you can also use generics with interfaces to create more flexible and reusable contracts for your code. For example, consider an interface for a dictionary data structure, where keys are strings and values can be of any type:

interface Dictionary<T> {
  [key: string]: T;
}

const numberDictionary: Dictionary<number> = {
  one: 1,
  two: 2,
  three: 3,
};

const stringDictionary: Dictionary<string> = {
  one: "one",
  two: "two",
  three: "three",
};

In this example, we're using the generic type parameter T to define the type of values that the dictionary can store. This allows us to create dictionaries with different value types while maintaining type safety.

Conclusion

Generics are a powerful feature of TypeScript that allows you to write more reusable, flexible, and type-safe code. By using generics with functions, classes, and interfaces, you can create code that works with a variety of data types, without having to write separate implementations for each type. This can help you reduce code duplication, improve maintainability, and take full advantage of TypeScript's strong typing system.

As you continue learning about TypeScript and programming in general, keep in mind the concept of generics and how they can help you write more efficient and versatile code. And remember, practice makes perfect, so try implementing generics in your own projects to get a better understanding of how they work and how they can benefit your code.