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.

Senior Go Backend Developer (Advertising Dashboard Team) (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 anadvanced tech stack, powerful financial backing from Bertelsmann, and a highly motivated workforce to be reckoned with.

Meet Your Team: Advertising Dashboard Team (part of the Demand Solutions team)

The Demand Solutions department builds the core technologies to help mobile publishers acquire the right users for their apps and games. The team is proud that adjoe’s software – developed 100 percent in-house – manages, provides, and analyzes advertisements for more than 200 million daily mobile users.adjoe’s Demand Solutions department builds modern user interfaces and software and powers its analytics in the dashboard with state-of-the-art databases like Druid. All to give advertisers and account managers the insights they need. The team provides statistical data via a dashboard in order to supply our partners with valuable insights. For this we leverage machine learning models built by our BI analysts and data scientists. Besides delivering high standards, Demand Solutions remains flexible and autonomous from adjoe’s other tech teams. To develop new platform features fast, this versatile team works on adjoe’s Go backend, which feeds data to the TypeScript React frontend.

As part of the Demand Solutions team, the Advertising Dashboard team is responsible for development of User & API interfaces that we provide for our internal business team and external advertisers to create, launch, analyze and optimize advertising campaigns.
What You Will Do
  • Contribute to the development of our backend written +99% in Go and maintain our microservice architecture used to communicate with our frontend (based on TypeScript React). To do this, you’ll use event buses like Kafka and SQS/SNS 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 columnar databases such as Druid – but also with MySQL, where you’ll optimize queries and the way we query data to deliver few-millisecond response times
  • Support partners by providing them with raw or aggregated data based on their business needs – we believe in data transparency and well-documented open APIs
  • 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
  • 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 worked in software development for 8+ years.
  • You have gained profound experience with building web applications in Go for at least 5 years.
  • You have worked with at least one columnar database (Druid, ClickHouse, Snowflake, Vertica, …) and know how to analyze and optimize queries for it
  • You know how to profile a go application and figure out bottlenecks and have used this already in your work to optimize the code of the application
  • You have worked on a large go application with a considerable amount of traffic (>100e6 requests per day)
  • You are open to relocating to Hamburg, Germany
  • Plus: You have experience working with infrastructure as code (Terraform), Docker, and serverless infrastructure
  • 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
  • Skip writing cover letters. Tell us about your most passionate personal project, your desired salary and your earliest possible start date. We are looking forward to your application!

    We welcome applications from people who will contribute to the diversity of our company.

    Drive solutions, thrive in our team

    See vacancies