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.
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.
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.
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!
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 };
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 }
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:
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.
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.
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 and mobile apps.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
2 Replies to "Getting started with TypeScript generics"
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.
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.