Introduction
Over the course of the last few years, Typescript has emerged as the de-facto standard for writing type-safe Javascript code. This added type-safety gives developers confidence in their code and helps them catch errors even before the code is executed.
The best part of Typescript is
Every JavaScript program is also a TypeScript program
What this means is that you can start writing Typescript code using just the basic features of the language. You don't need to use all the advanced features right from Day 1. You can keep learning more advanced features and use them in your codebase on the go.
One such advanced and super powerful feature of Typescript is Generics
What are generics?
Generics are like any other variable that you define and store value in. However, generics are special because the value that can be stored in these variables is the type
information. For example - number
, string
etc.
Confused? Let's try to understand it with a simple example.
In the below code snippet, we have defined an array with a type of number[]
. Sound familiar right?
const arr: number[] = [1,2,3,4];
Another way to write these using built-in generics is below
const arr: Array<number> = [1,2,3,4];
Here, we passed the type number
as a variable to Array
. We can actually pass any type like this. For example
const strArr: Array<string> = ['a', 'b', 'c'];
const boolArr: Array<boolean> = [true, false, true];
The above are the simplest examples of using generics in typescript.
What's the major use case for generics?
Let's try to understand this with the help of an example. Suppose we need to write a function that takes a list of numbers and returns a comma-separated string of those numbers.
Input: [1,2,3,4,5]
Output: "1,2,3,4,5"
We can write a simple function in typescript for this as below
function getListAsString(arr: number[]) {
// Null check
if (!arr || arr.length === 0) {
return '';
}
// Add first element to string
let str = `${arr[0]}`;
// Add remamining elements with comma
for(let i = 1; i < arr.length; i ++){
str = `${str},${arr[i]}`;
}
return str;
}
console.log(getListAsString([1,2,3,4,5]));
This works perfectly for the argument of type number[]
. Now let's say you want to reuse the same function for string arrays as well.
Input: ['apple','bat','cat']
Output: "apple,bat,cat"
However, since the input type of arr
is defined as number[]
currently, the typescript compiler will throw an error.
Well, one option is to modify the type to be (number | string)[]
. Another option is to use the good old friend any[]
here. However, with both the options, you would lose out on the type checking benefits. The function would start accepting arrays of mixed types as well. For example, getListAsString(['apple','bat','cat', 5]))
would also not throw an error.
This is exactly the use case of generics. We can pass the type information while calling the function getListAsString
.
Using generics in the function
We can declare a generic T
(for type) in the function below
function getListAsString<T>(arr: T[]) {
// Null check
if (!arr || arr.length === 0) {
return '';
}
// Add first element to string
let str = `${arr[0]}`;
// Add remamining elements with comma
for(let i = 1; i < arr.length; i ++){
str = `${str},${arr[i]}`;
}
return str;
}
Here we are telling typescript that T
will have a type and the argument provided will be an array of that type. If the value of T
is number
, the argument will be of type number[]
. If the value of T
is string
, the argument will be of type string[]
.
We can pass the value of generic type T
using <>
while calling the function.
For example, here we have passed the type T
as number
getListAsString<number>([1,2,3,4,5])
Similarly, we can pass string
as below
getListAsString<string>(['apple','bat','cat'])
Can we pass multiple generics to a function?
Why not? You can pass any number of generics to a function.
For example, let's say we want to create a function that merges two arrays of different types.
Input: ['a','b','c'], [1,2,3]
Output: ['a','b','c', 1,2,3]
We can do something like below. Try to go through it slowly to understand whats happening here.
function mergeTwoArrays<T, U>(arr1: Array<T>, arr2: Array<U>) {
return [...arr1, ...arr2];
}
console.log(mergeTwoArrays<string, number>(['a','b','c'], [1,2,3]))
So here while calling the function mergeTwoArrays
, we are passing two generics i.e. string
and number
. These get assigned to T
and U
respectively. So finally the type of arr1
is becomes Array<string>
and that of arr2
becomes Array<number>
.
Defining type constraints
Another powerful application of generics is to define constraints on the types using generics.
For example, you want to create a function that returns the length of something
. This something
could be an array
or a string
or any other data type which has a .length
defined.
We can put such a constraint as below
interface TypesWithLength {
length: number;
}
function getLength<T extends TypesWithLength>(val: T) {
return val.length;
}
getLength<number[]>([1,2,3]);
getLength<string>("Hello");
getLength<number>(1); // Type 'number' does not satisfy the constraint 'TypesWithLength'
Here on the first line, we defined an interface TypesWithLength
with a single property length
of type number
. So any value which adheres to this interface needs to have a length
property of type number
.
Further, we extended the type T
and put this constraint on that type using T extends TypesWithLength
. Now, this function will accept only arguments which adhere to the interface TypesWithLength
. That's why the last line where we passed the type as number
throws the error.
Conclusion
Generics is one of the most powerful features of typescript. While this was just an introduction to generics where we covered a few of the use cases where generics could make our life easy, I would strongly urge you to read more about it and start using them in your codebase.
Thank you for reading, hope you found this useful. Let me know in the comments below.
Happy coding!