Skip to main content

Getting started with TypeScript generics

TypeScript came to solve the problem of types in JavaScript for us by bringing optional static type-checking in JavaScript. It is fairly easy to get started with TypeScript, in just a few minutes you can have a TypeScript project running along with your first types. But after using TypeScript and creating your first union types, you might start to notice a problem. You have a lot of similar types that have been created just for some specific piece of code.

In other words, you’re creating more code than you need. That’s something that will not help you scale your code long term. But how can you solve this problem?

For this, we have generics in TypeScript. You don’t need to create a lot of types, you can benefit from the usage of generics to have more reusable code.

Generics

TypeScript can increase the code complexity of your project if you’re using it the wrong way. When you have more types, interfaces, and union types than you need, your code will get more difficult to read and maintain.

To understand what generics are and how it works, let’s start with an example. Let’s imagine that we have a function called print, this function is responsible for simply returning our user:

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

const user: User = {
  name: "Leonardo",
  age: 22
};

function print(user: User): User {
  return user;
};

As you can see, there is nothing too difficult or unusual for now. We create a type called User, an object called user which we specified with our User type. Also, in our print function, we’re going to receive a user argument that needs to be equal User, and as the final result, our function is going to return a User.

Now, let’s imagine that we want to change something in our function. Instead of returning a User, we’re going to need to return something similar to User, but a little different.

Let’s imagine that we have a type called Admin, that’s similar to User but has an additional property:

type Admin = {
  name: string;
  age: number;
  admin: true;
};

We now have a new type, and for reusability purposes, it would be better if our print function allows us to print both User and Admin types.

This is the perfect use case to create a generic type. With generics, we can work with a variety of types instead of a single one. In our case, we can use our print function to print our User and Admintypes.

This is how we can create a generic type for a function. After the function name, we pass our generic type between the <>. In our case, we’re going to name our generic type T. The argument of our function is also going to be the generic type that we created, and the return of our function is also going to be our generic type. We’re using T as our generic type, this generic type allows us to work with a variety of types instead of a single one.

You might be wondering here, ‘but why T?’ T stands for Type, and it’s commonly used by TypeScript to refer to the first type variable name. You can name it whatever you would like, but for type variables, when using generics, it’s a common practice (especially when you have more than one generic type) to name it another single letter.

Now, if we call our print function to print both user and adminobjects, it will work perfectly. We’re using a generic type to make sure that we’re printing an object with the correct properties, and we can notice that our code gets more readable and reusable:

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

type Admin = {
  name: string;
  age: number;
  admin: true;
};

const user: User = {
  name: "Leonardo",
  age: 22
};

const admin: Admin = {
  name: "Leonardo",
  age: 22,
  admin: true
};

function print<T>(user: T): T {
  return user;
};    

print<User>(user); // Result: { name: "Leonardo", age: 22 }
print<Admin>(admin); // Result: { name: "Leonardo", age: 22, admin: true }

A thing to notice here is that generics allow us to work with a variety of types instead of a single one, and when we’re going to invoke or instantiate our code, we need to pass our type as a “parameter”.

In our previous example, when we invoked the print function to print a user, we need to pass the user type as a parameter, if we didn’t pass anything, it would work the same but without the benefits of TypeScript.

Classes

We can work with generic classes in TypeScript. Another nice thing about generics is that we can have multiple type parameters. Let’s imagine that we have a class called User, and we want to pass two type parameters for this class — one for name and another one for age. This is how we could do it:

class User<T, U> {
  name: T;
  age: U;
  constructor(name: T, age: U) {
    this.name = name;
    this.age = age;
  }

  print(): void {
    console.log(`Hello ${this.name}, you are ${this.age} years old!`);
  }
};

const newUser = new User<string, number>("Leonardo", 23);
newUser.print(); // Hello Leonardo, you are 23 years old!

Interfaces

Another nice thing about generics is that we can create generic interfaces, we can easily create reusable interfaces and use different types and interfaces.

Let’s imagine that we have three interfaces AdminUser, and Client. We’re going to pass our generic type parameter on our Admin interface, and another two interfaces that we’re going to use for testing purposes:

interface User {
  firstName: string;
};

interface Client {
  firstName: string;
  lastName: string;
};

interface Admin<T> {
  values: T;
  isAdmin: true;
};

Our Client interface is different from our User interface. Now, let’s create two different objects and pass different interfaces for our generic interface Admin:

interface User {
  firstName: string;
};

interface Client {
  firstName: string;
  lastName: string;
};

interface Admin<T> {
  values: T;
  isAdmin: true;
};

const user: Admin<User> = {
  values: {
    firstName: "Leonardo"
  },
  isAdmin: true
};

const client: Admin<Client> = {
  values: {
    firstName: "Leonardo",
    lastName: "Maldonado"
  },
  isAdmin: true
};

Constraints

When we’re using generics, we can use constraints to apply to our generic type parameters. To implement it, all we have to do is use the extends keyword. Before using it, you need to make sure of something — the extends keyword only works with interfaces and classes, otherwise, it will throw an error.

Let’s take our first print function as an example, and apply a constraint using the extends keyword in our T type parameter:

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

const user: User = {
  name: "Leonardo",
  age: 22
};

function print<T>(user: T): T {
  return user;
};
print<User>(user); // Result: { name: "Leonardo", age: 22 }

When to use generics?

Once you feel more comfortable with generics and understand how it works, you can start to use it more and hopefully realize the real benefits of it. But before you start to use it everywhere, you need to identify when to use it to create a performative piece of code.

To identify when to use a generic type, you can consider two things:

  1. Will this function or class be required to work with a variety of data types?
  2. Will this function or class be used in more than one place?

These are two questions that can help you to decide if you’re going to need generics or not. As your application grows, the chances that you’re working and creating more types than you need might increase. Something we might forget to consider is to use something like generics to help us have more reusable and maintainable code.

In the long-term, not only will you benefit from it, but your whole team will. Your code will get more readable, that means that other developers can easily understand and maintain it if needed, your code will get more concise and well-written, and you will feel that your application is more secure and consistent than ever.

Conclusion

In this article, we learned how we can use generic types in our functions to create more reusable functions. We also learned about generic classes and generic interfaces. The concept of generics is one of the best when you’re trying to achieve a more reusable and maintainable code, it makes your life easier to reuse types without having to create a few similar types.

Comments

Popular posts from this blog

4 Ways to Communicate Across Browser Tabs in Realtime

1. Local Storage Events You might have already used LocalStorage, which is accessible across Tabs within the same application origin. But do you know that it also supports events? You can use this feature to communicate across Browser Tabs, where other Tabs will receive the event once the storage is updated. For example, let’s say in one Tab, we execute the following JavaScript code. window.localStorage.setItem("loggedIn", "true"); The other Tabs which listen to the event will receive it, as shown below. window.addEventListener('storage', (event) => { if (event.storageArea != localStorage) return; if (event.key === 'loggedIn') { // Do something with event.newValue } }); 2. Broadcast Channel API The Broadcast Channel API allows communication between Tabs, Windows, Frames, Iframes, and  Web Workers . One Tab can create and post to a channel as follows. const channel = new BroadcastChannel('app-data'); channel.postMessage(data); And oth...

Certbot SSL configuration in ubuntu

  Introduction Let’s Encrypt is a Certificate Authority (CA) that provides an easy way to obtain and install free  TLS/SSL certificates , thereby enabling encrypted HTTPS on web servers. It simplifies the process by providing a software client, Certbot, that attempts to automate most (if not all) of the required steps. Currently, the entire process of obtaining and installing a certificate is fully automated on both Apache and Nginx. In this tutorial, you will use Certbot to obtain a free SSL certificate for Apache on Ubuntu 18.04 and set up your certificate to renew automatically. This tutorial will use a separate Apache virtual host file instead of the default configuration file.  We recommend  creating new Apache virtual host files for each domain because it helps to avoid common mistakes and maintains the default files as a fallback configuration. Prerequisites To follow this tutorial, you will need: One Ubuntu 18.04 server set up by following this  initial ...

Working with Node.js streams

  Introduction Streams are one of the major features that most Node.js applications rely on, especially when handling HTTP requests, reading/writing files, and making socket communications. Streams are very predictable since we can always expect data, error, and end events when using streams. This article will teach Node developers how to use streams to efficiently handle large amounts of data. This is a typical real-world challenge faced by Node developers when they have to deal with a large data source, and it may not be feasible to process this data all at once. This article will cover the following topics: Types of streams When to adopt Node.js streams Batching Composing streams in Node.js Transforming data with transform streams Piping streams Error handling Node.js streams Types of streams The following are four main types of streams in Node.js: Readable streams: The readable stream is responsible for reading data from a source file Writable streams: The writable stream is re...