One of the most important things when writing scalable applications is ensuring that the components that make up those applications are scalable and, thus, reusable by themselves.

When working with TypeScript, one of the utilities that ensure units of work are reusable and scalable is Generics.

Introduction to TypeScript’s Generics

Generics are not exclusive to TypeScript; they are a feature of statically typed language that allows developers to pass types as parameters to other types, functions, classes, or structures.

When creating a generic component, it is given the ability to accept and enforce a typing that will be passed in whenever that generic component is used. This improves flexibility and reusability and removes the need for code duplication.

TypeScript uses this feature to introduce type safety into components that accept arguments and return values whose types will be determined when consumed.

Generics Syntax

Before seeing Generics in action, we’ll first go over the syntax, after which we’ll see them in action.

Generics appear within angle brackets under the <T> format, where T is a passed-in type and can be read as a generic of type T.

The types specified inside angle brackets are also known as generic type parameters or just type parameters. Multiple generic types can also appear in a single definition, like <T, K, A>.

The T letter is simply a convention and does not affect functionality in any way; just as we’ve used the letter T, we can also use letter X, or even a whole word such as GenericType inside angle brackets.

It is recommended, however, to use the letter T when working with generics as it’s what most developers will be familiar with when expecting to work with them.

Example

Let’s imagine we have a function that takes an array as an argument and returns the last element of said array. That function would likely look something like this:

const getLastArrayElm = (arr) => {
  const arrLength = arr.length;
  const lastArrayElm = arr[arrLength - 1];

  return lastArrayElm;
};

That looks good. Now, if we were to test this function directly, we would see that it works:

Vague Parameter Type (any[])
Vague Parameter Type (any[])

With the test array that we’ve passed to the getLastArrayElm we would expect the last element to be the true boolean value, which indeed it is.

The issue, however, is that we had to pass in the arr parameter as of type any[], and the type of the lastTestArrElm variable would be any as well:

Vague Return Type (any)
Vague Return Type (any)

And that’s not very useful if we were to need this function someplace else.

Generics Implementation

The way to implement a type-safe function is to wrap it within a Generic, ensuring that the parameters’ types depend on the usage.

In our case, the implementation would look something like this:

const getLastArrayElm = <T>(arr: Array<T>): T => {
  const arrLength = arr.length;
  const lastArrayElm = arr[arrLength - 1];

  return lastArrayElm;
};

Now, when using the function, the types will be inferred based on the usage:

Inferred Type Based On Usage - With Generics
Inferred Type Based On Usage – With Generics

By using this approach, we are making our code much more predictable and, thus, easier to work with throughout our codebase.

Advanced Usage Of Generics

Now that we are a bit more comfortable with the concept of Generics let’s look at a more complex example of where we might need them.

Data Fetching With Generics

Let’s say we need to retrieve data from an external API; that’s a common use case.

We would likely have an asynchronous function that sends a request over to an API in the hopes of giving us some data back.

However, once we get the data, we don’t know what it looks like until we either log it to the console or check it out in the documentation, which is pretty annoying and will also deprive us of any hints inside our IDE.

No Type Inferrance
No Type Inference

If we look at the example above, we don’t know anything about the data variable, which can leave us with the possibility of human mistakes when trying to access properties or methods of the data.

To solve this problem, we’ll have to make use of Generic types and type definitions:

Typed Axios Request

Axios’s get method supports Generic Types, meaning that if we pass it a generic type, it will make that part of the response type:

Inferred Response Type
Inferred Response Type

Todo type according to JSONPlaceholder:

type Todo = {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
};

By making this change, we can now get autocompletion and hints on the retrieved data:

Default Typed Params

There are scenarios where, despite using generics, we cannot cover all of the edge cases. Thus, we will need to use a fallback type to maintain the type inference and predictability that TypeScript provides us with.

The syntax would look something like this:

const myFunc<T = {}>(x: T) => { ... };

Summary

I hope you’ve enjoyed the read and better understood what Generics are, how they are used, and some scenarios where we would use them.

Let me know your thoughts on this article by leaving a comment below.

Cheers!

👋 Hey, I'm Vlad Mihet
I'm Vlad Mihet, a blogger & Full-Stack Engineer who loves teaching others and helping small businesses develop and improve their technical solutions & digital presence.

💬 Leave a comment

Your email address will not be published. Required fields are marked *

We will never share your email with anyone else.