TypeScript Arrow Functions with Generic Types: A Practical Guide

Updated: January 8, 2024 By: Guest Contributor Post a comment

Introduction

Embracing TypeScript’s type system can elevate your code to new levels of robustness and clarity. This guide walks you through the practical use of arrow functions with generic types, a technique that enhances reusability and ensures type safety across diverse functions in TypeScript.

Understanding Arrow Functions

Before diving into generics, let’s review arrow functions. They are a concise way to write function expressions in JavaScript, and TypeScript inherits this feature with added type capabilities. An arrow function looks like this:

const greet = (name: string): string => `Hello, ${name}!`;
console.log(greet('Alice')); // Output: Hello, Alice!

Arrow functions also inherently bind this to the context of where the function is created, unlike traditional functions.

Generic Types Basics

Generics add the ability to create components and functions that work over a variety of types rather than a single one. Here’s a simple generic function:

function identity<T>(arg: T): T {
  return arg;
}

This function accepts one argument, arg, of type T, and simply returns it. The beauty here is that T can be any type, and it’s determined when the function is invoked.

Arrow Functions with Generics

Combining arrow functions with generics, we get the best of both worlds – the expressiveness of arrow syntax with the dynamic nature of generics. Here’s an example:

const identityArrow = <T>(arg: T): T => arg;

This is the arrow function version of the earlier identity function.

Using Generics for Callbacks

Arrow functions are often used as callbacks. Here is how you can define a function that takes a generic callback:

function processItems<T>(items: T[], callback: (item: T) => void): void {
  items.forEach(callback);
}

Working with Multiple Generic Types

You’re not limited to a single generic type. Here’s how you can work with multiple:

const mergeArrays = <A, B>(array1: A[], array2: B[]): (A | B)[] => {
  return [...array1, ...array2];
};

This function can merge two arrays with potentially different types into a single array.

Constraints on Generic Types

Sometimes, you will want to limit the types a generic function can accept:

const getProperty = <T, K extends keyof T>(obj: T, key: K) => obj[key];

Here we ensure that K, the type of the key, is a key of the object type T.

Generic Type Defaults

You can also define default types for your generics:

const createArray = <T = string>(length: number, fill: T): T[] => {
  return new Array(length).fill(fill);
};

Now, if the type isn’t specified during the function invocation, it defaults to string.

Advanced Patterns

Let’s explore some advanced uses of generics and arrow functions.

Inferring Generic Types

TypeScript is smart enough to infer the generic types based on the arguments provided:

const numbers = identityArrow([1, 2, 3]); // TypeScript infers T as number[]

However, you can always specify the type explicitly for more control or clarity.

Generic Types in Object Methods

Arrow functions with generics aren’t limited to stand-alone functions. They can be methods in an object too:

const storage = {
  setItem: <T>(key: string, item: T): void => {
    localStorage.setItem(key, JSON.stringify(item));
  },
  getItem: <T>(key: string): T | null => {
    const data = localStorage.getItem(key);
    return data ? JSON.parse(data) : null;
  }
};

In the storage object, both setItem and getItem are generic arrow functions, providing a type-safe interface to the localStorage API.

Higher-Order Generics

Arrow functions can also return new functions, creating higher-order functions:

const makeAdder = <T extends number>() => (x: T) => (y: T): T => x + y;
const addFive = makeAdder()(5);
console.log(addFive(10)); // Output: 15

In this example, makeAdder is a higher-order arrow function using generics.

Conclusion

Through this guide, we’ve explored the versatility and power of combining arrow functions with generic types in TypeScript. By mastering these concepts, you will enhance the maintainability and type safety of your codebase, allowing for greater flexibility and reducing the risk of runtime errors.