Generics in Typescript and how to use them

Lucy Love - Jun 10 - - Dev Community

Sometimes I wonder whether I love or hate Typescript. I see the benefits of implementing it in the majority of frontend solutions, but I struggle with the syntax and the mumbo jumbo names that don't tell me anything. "Generics" would be one of those, as the name kindly suggests too, it's a name that could mean anything 🤷 When I find myself in the situation where I think I have grasped something but I haven't quite, the best solution is to try to write something about it to bring in clarity!

So what are Generics?

Generics in TypeScript provide a way to create reusable data structures (arrays, stacks, queues, linked lists etc.), components and functions that can work with different data types.

Take for example:

interface Dictionary<T> {
  [key: string]: T;
}
Enter fullscreen mode Exit fullscreen mode

With generics you can create a container type that can hold different types of values based on needs, e.g.:

const myDictionary: Dictionary<number> = {
  age: 30,
  height: 180,
};

const myOtherDictionary: Dictionary<string> = {
  name: 'John',
  city: 'New York',
};
Enter fullscreen mode Exit fullscreen mode

This is useful when you want to create a flexible and type-safe container, such as a dictionary or a set. You'll be guaranteed, whether the structure will accommodate strings or numbers, that it will follow the interface Dictionary and expect an object with keys whose value will reflect the data you use it with (=== if you have Dictionary<string> the values of the keys can only be string, if you have Dictionary<number> the values of the keys can only be number).

Let's make another practical example; let's say you want to have a function that allows you to print whatever type of array in a particular format, like an ordered list. In this case, the type of the data passed is not relevant to you, you only care that it's presented in an ordered list.

function printArray<T>(array: T[]): string {
  const items: string = 
    array.map(item => `<li>${item}</li>`).join('');
  return `<ol>${items}</ol>`;
}
Enter fullscreen mode Exit fullscreen mode

Now you can use this function to format the arrays before appending them to a div:

function printArray<T>(array: T[]): string {
  const items: string = 
    array.map(item => `<li>${item}</li>`).join('');
  return `<ol>${items}</ol>`;
}

// An example function to append HTML content to a div element
function appendToDiv(
  containerId: string, 
  content: string
): void {
  const container = document.getElementById(containerId);
  if (container) {
    container.innerHTML = content;
  }
}

// Our data types
const numbers: number[] = [1, 2, 3, 4, 5];
const strings: string[] = ['apple', 'banana', 'orange'];

// Our data types formatted through the printArray function
const numbersListHTML: string = printArray(numbers);
const stringsListHTML: string = printArray(strings);

// Append the lists to an hypotetical div container
// that shows the list
appendToDiv('orderedLists', numbersListHTML);
appendToDiv('orderedLists', stringsListHTML);
Enter fullscreen mode Exit fullscreen mode

In the example, printArray doesn't care whether the array inputted is a numeric or a string one, but it makes sure to guarantee type safety based on the data type you pass to it.

Not bad, right?


What is a somewhat more practical application of generics in the real-life coding world?

Generics can be useful when working with promises and asynchronous operations, allowing you to specify the type of the resolved value.

// Function that returns a promise to fetch data from an API
function fetchData<T>(url: string): Promise<T> {
  return fetch(url)
    .then(response => {
      if (!response.ok) {
        throw new Error('Network response was not ok');
      }
      return response.json() as Promise<T>;
    })
    .catch(error => {
      console.error('Error fetching data:', error);
      throw error;
    });
}

// Usage
interface UserData {
  id: number;
  name: string;
  email: string;
}

const apiUrl = 'https://api.example.com/users/1';

fetchData<UserData>(apiUrl)
  .then(data => {
    // Here, 'data' will be of type UserData
    console.log('User Data:', data);
  })
  .catch(error => {
    console.error('Error fetching user data:', error);
  });
Enter fullscreen mode Exit fullscreen mode

While this example is not perfect, it offers us a great way to dynamically query for various URLs, and for each case we have the possibility to provide the type we expect to receive: we query for https://api.example.com/users/1 - we expect to receive a response containing user information - so we provide the interface UserData to the function fetchData<UserData>(apiUrl) and if the request doesn't fail, our response will be already using the correct interface. Neat!


Challenges with Generics

Not all that shines is gold, and the same can be said about generics. While they offer this great versatility, you can imagine they also provide a lot of complexity to a codebase, as they will require developers to pay better attention to the data flow and make sure that things won't go as unexpected.

Together with the broader learning curve proposed to developers, there are also various limitations of type inferring that might make generics limiting to a practical application. No idea about those limits, will let you know when I stumble on those :D

Long story short

If you use them, make sure it makes sense on why you're using them and that they actually bring benefits on what you're doing. If you're working on a npm package that needs to accommodate different solutions (idk, a table with columns that can sort according to the data type provided, where you might not know ahead of time what values will be entered), generics might be very well what you're looking for! Bear in mind however, that they will always create an extra layer of complexity that you need to account for your code and for all your team mates!

Sources and inspiration

. . . . . . . .