Generics in TypeScript: A Complete Guide (Part 1)

Generics in TypeScript: A Complete Guide (Part 1)

Β·

6 min read

Generics are a powerful feature of TypeScript that allow you to write flexible and reusable code. In this blog, I will explain what generics are, why they are useful, and how to use them in your TypeScript projects.

Let's go! 🌟

What are generics and why do we need them?

Let’s start with a simple example. Suppose you have a function that takes an array of numbers and returns the first element of the array. You can write it like this πŸ‘‡

function getFirstElement(array: number[]): number {
  return array[0];
}

This function works fine if you pass an array of numbers to it πŸ‘‡

let numbers = [1, 2, 3];
let firstNumber = getFirstElement(numbers); // firstNumber is inferred to be number
console.log(firstNumber); // 1

But what if you want to use the same function for an array of strings? You might try to pass an array of strings to it πŸ‘‡

let strings = ["a", "b", "c"];
let firstString = getFirstElement(strings); // Error: Argument of type 'string[]' is not assignable to parameter of type 'number[]'.
console.log(firstString);

You will get an error, because the function expects an array of numbers, not an array of strings. You could try to fix this by changing the type of the array parameter to any[]:

function getFirstElement(array: any[]): any {
  return array[0];
}

This will make the function accept any array, but it will also make the return type of the functionany. This means that TypeScript will not be able to infer the type of the variable that stores the returned value, like this:

let numbers = [1, 2, 3];
let firstNumber = getFirstElement(numbers); // firstNumber is inferred to be any
console.log(firstNumber); // 1

let strings = ["a", "b", "c"];
let firstString = getFirstElement(strings); // firstString is inferred to be any
console.log(firstString); // a

This is not ideal, because we lose the type safety and the code completion features that TypeScript provides. We know that the first element of the numbers array is a number, and the first element of the strings array is a string, but TypeScript does not know that. πŸ‘€

Another possible solution is to use a union type for the array parameter πŸ‘‡

function getFirstElement(array: (number | string)[]): number | string {
  return array[0];
}

This will make the function accept an array of either numbers or strings, and return either a number or a string. However, this will also make the return type of the function a union type πŸ‘‡

let numbers = [1, 2, 3];
let firstNumber = getFirstElement(numbers); // firstNumber is inferred to be number | string
console.log(firstNumber); // 1

let strings = ["a", "b", "c"];
let firstString = getFirstElement(strings); // firstString is inferred to be number | string
console.log(firstString); // a

This is better than using any, but still not ideal, because we introduce unnecessary uncertainty in the type of the returned value. We know that the first element of the numbers array is always a number, and the first element of the strings array is always a string, but TypeScript doesn't know that.

This is where generics come in handy! πŸ’«

How to use generics in TypeScript?

Generics are a way of writing functions (or classes, interfaces, etc.) that can work with different types of data, without losing the type information. You can think of generics as placeholders for types that will be determined later, based on the input or output of the function.

To define a generic function, you use angle brackets (<>) after the name of the function, and give a name to the generic type. For example, you can write a generic version of the getFirstElement function like this πŸ‘‡

function getFirstElement<ElementType>(array: ElementType[]): ElementType {
  return array[0];
}

Here, we have defined a generic type called ElementType, which can be any type. We have used this type to specify the type of the array parameter and the return type of the function. This means that the function will accept an array of any type, and return the first element of the same type.

Now, we can use the generic function for both numbers and strings, like this:

let numbers = [1, 2, 3];
let firstNumber = getFirstElement(numbers); // firstNumber is inferred to be number
console.log(firstNumber); // 1

let strings = ["a", "b", "c"];
let firstString = getFirstElement(strings); // firstString is inferred to be string
console.log(firstString); // a

Notice how TypeScript is able to infer the type of the returned value based on the type of the array that we pass to the function. This is because TypeScript replaces the generic type ElementType with the actual type (number and string in our case respectively) that we use.

We can also explicitly specify the generic type when we call the function, like this:

let numbers = [1, 2, 3];
let firstNumber = getFirstElement<number>(numbers); // firstNumber is number
console.log(firstNumber); // 1

let strings = ["a", "b", "c"];
let firstString = getFirstElement<string>(strings); // firstString is string
console.log(firstString); // a

This is useful when TypeScript is not able to infer the generic type automatically, or when we want to be more explicit about the type.

Using multiple generic types

We can also use more than one generic type in a function, if we need to. For example, we can write a function that takes two arrays of different types and returns an array of pairs πŸ‘‡

function returnPairs<FirstType, SecondType>(firstArray: FirstType[], secondArray: SecondType[]): [FirstType, SecondType][] {
  let result: [FirstType, SecondType][] = [];
  for (let i = 0; i < Math.min(firstArray.length, secondArray.length); i++) {
    result.push([firstArray[i], secondArray[i]]);
  }
  return result;
}

Here, we have defined two generic types, FirstType and SecondType, which can be any types. We have used these types to specify the types of the two array parameters and the return type of the function. This means that the function will accept two arrays of different types, and return an array of pairs of the same types.

Now, we can use the generic function for different types of arrays:

let numbers = [1, 2, 3];
let strings = ["a", "b", "c"];
let pairs = returnPairs(numbers, strings); // pairs is inferred to be [number, string][]
console.log(pairs); // [[1, "a"], [2, "b"], [3, "c"]]

let booleans = [true, false, true];
let dates = [new Date(), new Date(), new Date()];
let pairs2 = returnPairs(booleans, dates); // pairs2 is inferred to be [boolean, Date][]
console.log(pairs2); // [[true, Date object], [false, Date object], [true, Date object]]

So, here we are!

In this blog, we learned what generics are and how to use them in functions.

In the next part, we will see how to use generics in types and some more advanced usages of Generics.

Next part πŸ‘‰ here.

Β