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 letterT
, we can also use letterX
, or even a whole word such asGenericType
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:
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:
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:
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.
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:
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!
💬 Leave a comment