Leonardo Maldonado Full Stack Developer. JavaScript, React, TypeScript, GraphQL.

Getting started with TypeScript generics

5 min read 1414

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 Admin types.

We made a custom demo for .
No really. Click here to check it out.

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 admin objects, 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 Admin, User, 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.

You come here a lot! We hope you enjoy the LogRocket blog. Could you fill out a survey about what you want us to write about?

    Which of these topics are you most interested in?
    ReactVueAngularNew frameworks
    Do you spend a lot of time reproducing errors in your apps?
    YesNo
    Which, if any, do you think would help you reproduce errors more effectively?
    A solution to see exactly what a user did to trigger an errorProactive monitoring which automatically surfaces issuesHaving a support team triage issues more efficiently
    Thanks! Interested to hear how LogRocket can improve your bug fixing processes? Leave your email:

    : Full visibility into your web apps

    LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

    In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

    .
    Leonardo Maldonado Full Stack Developer. JavaScript, React, TypeScript, GraphQL.

    One Reply to “Getting started with TypeScript generics”

    1. Good morning! Thanks for writing a nice article on Generics. I noticed that before the last example, you forgot to actually use the ‘extends’ keyword. Have a nice day.

    Leave a Reply