DEV Community

Cover image for Using the TypeScript generic type to create reusable components
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Using the TypeScript generic type to create reusable components

Written by Ishan Manandhar✏️

What are TypeScript generics?

Generics in TypeScript are a method for creating reusable components or functions that can handle multiple types. Generics are a powerful tool that can assist us in creating reusable functions. They allow us to build data structures without needing to set a concrete time for them to execute at compile time.

In TypeScript, they serve the same purpose of writing reusable, type-safe code where the type of variable is known at compile time. This means that we can dynamically define the type of parameter or function that will be declared beforehand. This comes in really handy when we need to use certain logic inside of our application; with these reusable pieces of logic, we can create functions that take in and give out their own types.

We can use generics to implement checks at compile time, eliminate type castings, and implement additional generic functions across our application. Without generics, our application code would compile at some point, but we may not get the expected results, which could possibly push bugs into production. This powerful feature can help us create reusable, generalized, and type-safe classes, interfaces, and functions.

In this post, we’ll learn how to achieve type safety via generics without sacrificing performance or efficiency. We can write a type parameter between angle brackets: <T>. We can also create generic classes, generic methods, and generic functions in TypeScript.

Jump ahead:

TypeScript generics in action

Functions without using generics

Let’s consider the following example. Below, we have a simple function that will output the reverse order of what we provide in the array. We will name our function removeRandomArrayItem, which looks like:

function removeRandomArrayItem(arr: Array<number>): Array<number> {
 const randomIndex = Math.floor(Math.random() * arr.length);
 return arr.splice(randomIndex, 1);
}

removeRandomArrayItem([6, 7, 8, 4, 5, 6, 8, 9]);
Enter fullscreen mode Exit fullscreen mode

The function above tells us that the function name is removeRandomArrayItem and that it will take a parameter of item, which is a type of array consisting of numbers. Finally, this function returns a value, which is also an array of numbers.

As you can see, we have already introduced a few constraints inside of our code. Let’s say we want to loop through an array of numbers instead. Should we build another function to handle this use case?

No! Here is the sweet spot where TypeScript generics kicks in.

Functions using generics

Let’s take a look into the problem before we write our solution using generics. If we pass the above function an array of numbers, it would throw us this error:

Argument of type 'number[]' is not assignable to parameter of type 'string[]'
Enter fullscreen mode Exit fullscreen mode

We can fix this by adding any to our type declaration:

function removeRandomArrayItem(arr: Array<any>): Array<any> {
 const randomIndex = Math.floor(Math.random() * arr.length);
 return arr.splice(randomIndex, 1);
}

 console.log(removeRandomArrayItem(['foo', 1349, 6969, 'bar']));
Enter fullscreen mode Exit fullscreen mode

But there is no valid reason to use TypeScript if we aren’t dealing with any data types. Let’s refactor this piece of function to convert using generics.

function removeRandomArrayItem<T>(arr: Array<T>): Array<T> {
 const randomIndex = Math.floor(Math.random() * arr.length);
 return arr.splice(randomIndex, 1);
}

console.log(removeRandomArrayItem(['foo', 'bar']));
console.log(removeRandomArrayItem([45345, 3453]));
Enter fullscreen mode Exit fullscreen mode

Here, we denoted a type named <T>, which will make it to act more generic. This will hold the type of data that is received by the function itself.

TypeScript classes using generics

class Foo {
 items: Array<number> = [];

 add(item: number) {
   return this.items.push(item);
 }

 remove(item: Array<number>){
   const randomIndex = Math.floor(Math.random() * item.length);
   return item.splice(randomIndex, 1);
 }
}

const bar = new Foo();

bar.add(22);
bar.remove([1345, 45312613, 13453]);
Enter fullscreen mode Exit fullscreen mode

Here, we created a simple class named Foo, which contains a variable that is an array of numbers. We also created two methods: one that adds items to the array and one that removes a random element from the array.

This piece of code works well, but we have introduced the same problem as before: if we add or remove items in the array, the generic will only take in array of numbers. Let’s refactor this class to use a generic to accept a generic value, so we can pass any type to the argument.

class Foo<TypeOfFoo> {
 items: Array<TypeOfFoo> = [];

 add(item: TypeOfFoo) {
   return this.items.push(item);
 }

 remove(item: Array<TypeOfFoo>){
   const randomIndex = Math.floor(Math.random() * item.length);
   return item.splice(randomIndex, 1);
 }
}

const bar = new Foo();

bar.add(22);
bar.add('adfvafdv');
bar.remove([1345, 45312613, 13453]);
bar.remove([1345, 45312613, '13453']);
Enter fullscreen mode Exit fullscreen mode

With the use of generics inside classes, we have made our code much more reusable and DRY. This is where generics really shine!

Using generics inside TypeScript interfaces

Generics are not specifically tied to functions and classes. We can also use generics in TypeScript inside of a interface as well. Let’s take a look at an example of how can we use it in action:

const currentlyLoggedIn = (obj: object): object => {
 let isOnline = true;
 return {...obj, online: isOnline};
}

const user = currentlyLoggedIn({name: 'Ben', email: 'ben@mail.com'});

const currentStatus = user.online
Enter fullscreen mode Exit fullscreen mode

With the above lines written, we get an error with a squiggly line telling us that we cannot access the property of isOnline from the user:

Property 'isOnline' does not exist on type 'object'.
Enter fullscreen mode Exit fullscreen mode

This is primarily because the function currentlyLoggedIn does not know the type of object it is receiving through the object type we added to the parameter. We can get around this by making the use of a generic:

const currentlyLoggedIn = <T extends object>(obj: T) => {
 let isOnline = true;
 return {...obj, online: isOnline};
}

const user = currentlyLoggedIn({name: 'Ben', email: 'ben@mail.com'});

user.online = false;
Enter fullscreen mode Exit fullscreen mode

The shape of the object we are currently dealing with in our function can be defined in an interface:

interface User<T> {
 name: string;
 email: string;
 online: boolean;
 skills: T;
}

const newUser: User<string[]> = {
 name: "Ben",
 email: "ben@mail.com",
 online: false,
 skills: ["foo", "bar"],
};

const brandNewUser: User<number[]> = {
 name: "Ben",
 email: "ben@mail.com",
 online: false,
 skills: [2456234, 243534],
};
Enter fullscreen mode Exit fullscreen mode

Here is another example of how we can use a interface with generics. We defined an interface with Greet, which will take in a generic, and that specific generic will be the type that is passed in to our skills property.

With this, we can pass the desired value to the skills property in our Greet function.

interface Greet<T> {
 fullName: "Ben Douglass";
 skills: T
 messageGreet: string
}

const messageGreetings = (obj: Greet<string>): Greet<string> => {
 return {
   ...obj,
   messageGreet: `${obj.fullName} welcome to the app`,
 skills: 'sd'
 };
};
Enter fullscreen mode Exit fullscreen mode

Passing default generic values to generics

We can also pass in a default generic type to our generic. This is useful in cases where we don’t want to pass in the data type we are dealing with in our function by force. By default, we are setting it to a number.

function removeRandomArrayItem<T = number>(arr: Array<T>): Array<T> {
 const randomIndex = Math.floor(Math.random() * arr.length);
 return arr.splice(randomIndex, 1);
}
console.log(removeRandomArrayItem([45345, 3453, 356753, 3562345, 3567235]));
Enter fullscreen mode Exit fullscreen mode

This snippet reflects how we made use of the default generic type on our removeRandomArray function. With this, we are able to pass a default generic type of number.

Passing multiple generic values

If we want our reusable blocks of functions to take in multiple generics, we can do the following:

function removeRandomAndMultiply<T = string, Y = number>(arr: Array<T>, multiply: Y): [T[], Y] {
 const randomIndex = Math.floor(Math.random() * arr.length);
 const multipliedVal = arr.splice(randomIndex, 1);
 return [multipliedVal, multiply];
}
console.log(removeRandomAndMultiply([45345, 3453, 356753, 3562345, 3567235], 608));
Enter fullscreen mode Exit fullscreen mode

Here, we created a modified version of our previous function so that we can introduce another generic parameter. We denoted it with the letter Y, which is set to a default type of number because it will multiply the random number we picked from the given array.

Since we’re multiplying numbers, we are definitely dealing with a number type, so we can pass the default generic type of number.

Adding constraints to generics

Generics allow us to work with any data types that are passed as arguments. We can, however, add constraints to the generic to limit it to a specific type.

A type parameter can be declared that is limited by another type parameter. This will help us add constraints upon the object, making sure we don’t obtain a property that possibly doesn’t exist.

function getObjProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
 return obj[key];
}

let x = { name: "Ben", address: "New York", phone: 7245624534534, admin: false };

getObjProperty(x, "name");
getObjProperty(x, "admin");
getObjProperty(x, "loggedIn"); //property doesn't exist
Enter fullscreen mode Exit fullscreen mode

In the above example, we created a constraint to the second parameter the function receives. We can invoke this function with the respective arguments and everything will work, unless we pass a property name that does not exist in the object type with the value of x. This is how we can constrict object definition properties using generics.

Conclusion

In this post, we explored how we can use generics, and created reusable functions inside our codebase. We implemented generics to create a function, class, interface, method, multiple interfaces, and default generics.


LogRocket: Full visibility into your web and mobile apps

LogRocket signup

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.

Try it for free.

Top comments (1)

Collapse
 
ota200 profile image
O.T.A

This is a great article, definitely is going to be extremely helpful in the simplification and understandability of the app my team and I are working on.