adjoe Engineers’ Blog
 /  TypeScript Generics in React Components
abstract decorative image with a squiggly line element

TypeScript Generics in React Components

In a React application, it is highly probable that we, as adjoe’s frontend developers, have some components that are heavily used across all the sections of the app – our core components. However, as the application grows, interaction with such components will become complex. Especially, if they are not designed to be flexible enough to support different use cases. 

Fortunately, TypeScript Generics – being the powerful tool that it is – can help us to alleviate such pain.

Case Study: A List Component

Imagine we need to create a component that renders a list of options. Initially, we could start with something general enough to support different option data types, like this:

interface ListOption {
  id: string;
  label: string;
}

interface ListProps {
  options: ListOption[];
  onOptionClick: (option: ListOption) => void;
}

const List = ({ options, onOptionClick }: ListProps) => {
  return (
    <ul>
      {options.map((option) => (
        <li
          key={option.id}
          onClick={() => onOptionClick(option)}
        >
          {option.label}
        </li>
      ))}
    </ul>
  )
};

And we could use the component to render a list of any of our entities, such as User or App:

interface App {
  id: string;
  title: string;
  platform: 'ios' | 'android';
}

const apps: App[] = [
  { title: 'Cat Escape', platform: 'android', id: 'gg.sunday.catescape' },
  { title: 'Spinner Merge', platform: 'ios', id: 'id6443864930' },
];

const appOptions: ListOption[] = apps.map(app => ({ id: app.id, label: app.title }));

interface User {
  id: string;
  name: string;
  active: boolean;
}

const users: User[] = [
  { id: '1', name: 'Isabel', active: true },
  { id: '2', name: 'Andres', active: true },
];

const userOptions: ListOption[] = users.map(user => ({ id: user.id, label: user.name }));

const DashboardComponent = () => {
  ...
  return (
    <>
      <List options={appOptions} onOptionClick={handleAppClick} />
      <List options={userOptions} onOptionClick={handleUserClick} />
    </>
  );
}

But, as you might notice, we already at this point have a situation on our hands that isn’t ideal: Just look at user interaction onClick. If we need to perform any action with the original entity (e.g. User), we need to iterate the source array to retrieve the original entity before performing the action:

const handleAppClick = (appOption: ListOption) => {
  const appIdx = apps.findIndex(app => app.id === appOption.id);
  if (appIdx !== -1) {
	const app = apps[appIdx];
	executeActionWithApp(app);
  }
};
const executeActionWithApp = (app: App) => {...}

After some List use cases in different parts of the app, the amount of boilerplate code we have to write only to support integration with the consumer components will become rather annoying. The reason for this extra logic is that our List component only knows about ListOption, and it’s not able to support a more generic type. But we can improve this by using TypeScript Generics.

Using TypeScript Generics in the List Component

There are a couple of ways to solve this problem by using Generics. One of these is to add an additional field to the ListOption interface, where we can place the original data entity. Instead of choosing a fixed data type for this field (such as any or User), we use a generic type (in this instance, called T) that is passed to the interface as a parameter:

interface ListOption<T> {
  id: string;
  label: string;
  data: T;
}

See how the type T is not only present for the data field (data: T) but also for the interface definition interface ListOption<T>. This means that the type T is a “parameter” for the ListOption interface and will therefore be defined later by the interface’s consumer. 

Let’s update our userOptions and appOptions to adopt the changes:

const appOptions: ListOption<App>[] = apps.map(app => (
  { id: app.id, label: app.title, data: app })
);
const userOptions: ListOption<User>[] = users.map(user => (
  { id: user.id, label: user.name, data: user })
);

Our next step is to update the ListProps interface to support the new generic type:

interface ListProps<T> {
  options: ListOption<T>[];
  onOptionClick: (option: ListOption<T>) => void;
}

Note how ListProps now needs to have a generic type, too (also called T), so it can be forwarded to the ListOption interface. We have also set a constraint between options and onOptionClick, since both are referencing the same type T.

It means that if an instance of the List component has options of type ListOption<User>, onOptionClick must be used with ListOption<User>, too – not with ListOption<App>, for example, or the compiler will comply.

Before we use the List component with generics, the component definition has to be tweaked a bit:

const List = <T,>({ options, onOptionClick }: ListProps<T>) => {...}

Here, we’re adding the generic type to the arrow function by preceding it with <T,>. The comma before > is only needed in files using JSX and when using only one generic type.

The last step is to update the click-handlers:

const DashboardComponent = () => {
  ...
  const handleAppClick = (appOption: ListOption<App>) => {
    executeActionWithApp(appOption.data);
  };

  const handleUserClick = (userOption: ListOption<User>) => {
    executeActionWithUser(userOption.data);
  }

  return (
    <>
      <List options={appOptions} onOptionClick={handleAppClick} />
      <List options={userOptions} onOptionClick={handleUserClick} />
    </>
  );
}

What Are the Benefits of Using React Generic Components?

As demonstrated with the simple List component use case, we, as developers, could benefit a lot from introducing TypeScript Generics to some of our React generic components. We would achieve improvements such as:

  • Components can become more flexible regarding the types they support via props.
  • We can still achieve strong typing for every type we decide to use from the consumer side (like User).
  • We can remove some lines of code needed to support the non-generic components version from the project.

And this is only the beginning. More advanced use cases could take our React generic components to the next level. These include using Generic Constraints, Type Parameters in Generic Constraints, and more.

Backend Development Technical Lead (Demand Solutions) (f/m/d)

  • adjoe
  • Demand Solutions
  • Full-time
adjoe is a leading mobile ad platform developing cutting-edge advertising and monetization solutions that take its app partners’ business to the next level. Part of the applike group ecosystem, adjoe is home to an advanced tech stack, powerful financial backing from Bertelsmann, and a highly motivated workforce to be reckoned with.

Meet Your Team: Demand Solutions

We are connecting the advertiser to the end users and on the one hand give users ads that are interesting to them and on the other hand find users for advertisers that would like their product. In order to do that we have built APIs that currently serve 2bn requests per day at low latency and on the other hand integrated ML models built by our data science team in order to serve the most interesting ads to our users.

A large part of the demand solutions system is a dashboard that is used by our internal and external users in order to make configuration changes to advertiser’s campaigns but that is mainly used as a tool for analysis. For this our dashboard features sophisticated analysis tools in order to help us and our partners to gain insights into their business and how to improve.
Join a team that is excited about the latest technologies and is highly interested in data, security, cloud computing, and mobile operating systems.
What you will do:
  • Contribute to the development of our demand solutions backend written all in Go and maintain our microservice architecture used to communicate with our dashboard (based on TypeScript React). To do this, you’ll use event buses like Kafka, SQS/SNS, and Kinesis in order to have reliable asynchronous microservice communication.
  • Work in a community of developers with whom you’ll share knowledge and contribute to peer code reviews. 
  • Work with modern databases such as DynamoDB and ScyllaDB to deliver few-millisecond response times for our mobile APIs and Druid to support fast and insightful analysis in our dashboard.
  • Continuously improve our distribution in order to stay up-to-date with the latest changes on the Android and iOS platform.
  • Be responsible for collecting the billions of daily API events and aggregating them in our Kafka and Kinesis streams with the goal of querying them from the data lake in a matter of seconds. 
  • Design & maintain APIs used by our external partners for reporting and publishing configuration changes for advertisers like for example uploading new images and videos for ads.
  • Design & maintain APIs used by our mobile end users such that we can serve their traffic at scale. 
  • Be part of an international English-speaking team dedicated to scaling our adtech platform beyond our hundreds of millions of monthly active users.

  • Who you are:
  • You have gained several years’ experience as a software architect/senior software developer and/or tech team lead.
  • You have outstanding experience with Go (preferred), Rust, PHP, Java or C# applied in a professional backend environment. 
  • You have basic knowledge of Go (in case it is not your main programming language) and are eager to become a Go expert quickly. 
  • You are an expert in different technologies – for example, in 2 programming languages.
  • You thoroughly understand cloud infrastructures such as AWS, Azure, or Google Cloud
  • You feel comfortable managing people and have previously worked in a leadership role in a fast-changing environment
  • Plus: You have knowledge of JavaScript/TypeScript (preferably of 1 framework such as
  • React, Vue.js, or Angular)
  • Heard of Our Perks?
  • Work-Life Package: 2 remote days per week, 30 vacation days, 3 weeks per year of remote work, flexible working hours, dog-friendly kick-ass office in the center of the city.
  • Relocation Package: Visa & legal support, relocation bonus, reimbursement of German Classes costs, and more.
  • Happy Belly Package: Monthly company lunch, tons of free snacks and drinks, free breakfast & fresh delicious pastries every Monday
  • Physical & Mental Health Package: In-house gym with a personal trainer, various classes like Yoga with expert teachers & free of charge access to our EAP (Employee Assistance Program) to support your mental health and well-being
  • Activity Package: Regular team and company events, and hackathons.
  • Education Package: Opportunities to boost your professional development with courses and training directly connected to your career goals 
  • Wealth building: virtual stock options for all our regular employees
  • Drive solutions, thrive in our team

    See vacancies