A Complete Guide to Generic Functions in typeScript

Hemanta Sundaray

Published December 25, 2023


In TypeScript, generics are like "placeholders" that allow you to write code without knowing the exact type of data you'll be working with, and then later fill in those placeholders with the appropriate type when you actually use the code.

Consider the example below:

function arrayLength(x: number[]) {
  return x.length;
}
console.log(arrayLength([1, 2, 3])); // Output: 3

We have a function called arrayLength() that takes an array of numbers as a parameter and returns the array's length.

What if we want to pass an array of strings?

We could write another function, of course, and type annotate the parameter x as an array of strings, like this:

function arrayLengthStrings(x: string[]) {
  return x.length;
}
console.log(arrayLengthStrings(["hello", "world"])); // Output: 2

As you can see, the only difference between the arrayLength() function and the arrayLengthStrings() function is the type of the parameter x. They both do the same thing: find the length of the array. However, by creating a separate function for each type of array, we are duplicating code and making our codebase more complex.

This duplication of code becomes an issue as the number of variations of arrays increases. For example, if we need to find the length of arrays of other types like arrays of booleans, arrays of objects, etc., we would need to write separate functions for each type, resulting in redundant code and increased maintenance overhead.

This is where generics come in handy. They allow us to work with multiple types of data without duplicating code.

Generic Function Syntax

Let's take our arrayLength() function as an example and convert it to a generic function.

Step 1: Add type parameters in angle brackets

First, we add angle brackets (< >) immediately before the function's parameter list, within which we place our type parameter. This type parameter acts as a placeholder for the type that will be used within the function. Here, we'll use T as our type parameter:

function arrayLength<T>() {
  // Function body will go here
}

Note

There is nothing special about the letter T (which stands for "Type" or "Template"); it's simply a convention. You're free to use any other letter or even a multi-letter word as your type parameter. The key is to choose a name that makes your code easy to understand and maintain.

In the example above, T is a placeholder representing the type of the array elements. The angle brackets after the function name (<T>) signify that T is a generic type parameter.

Step 2: Annotate parameters with the type parameter

Next, we use T to annotate the type of the parameter x. In this case, x is an array where each element is of type T (x: T[]). The specific type for T will be inferred based on the argument passed to the function at the time of invocation:

function arrayLength<T>(x: T[]) {
  return x.length;
}

With these adjustments, arrayLength() can now handle arrays of various types:

console.log(arrayLength([1, 2, 3])); // Output: 3
console.log(arrayLength(["hello", "world"])); // Output: 2
console.log(arrayLength(["hello", "world", 1, 2])); // Output: 4

In the first example, T is inferred as number because we passed an array of numbers as the argument. In the second example, T is inferred as string because we passed an array of strings. In the third example, T is inferred as string | number, because we passed an array containing both strings and numbers.

We now have a single function that can work with arrays of any type. We no longer need to worry about what type of data the array contains when defining the function. The actual type is filled in when we invoke the function. This makes the code more reusable and avoids duplication.

Constraints in Generics

Sometimes, you want your generic function to only work with a certain subset of types.

Let's revisit our arrayLength function:

function arrayLength<T>(x: T[]) {
  return x.length;
}

Suppose we want this function to work only with arrays containing strings, numbers, or a combination of both. This is where constraints in generics come into play.

By using constraints, we essentially tell TypeScript, "Hey, T can be any type, but it must adhere to these specific criteria." We implement this using the extends keyword.

Here's how we can modify our function:

function arrayLength<T extends string | number>(x: T[]): number {
  return x.length;
}

In this revised version, T extends string | number means T can either be a string, a number, or both. This constraint ensures that our function will not accept arrays of any other type.