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.

QA Engineer (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

The Demand Solutions team 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 team builds modern user interfaces and software and powers its analytics in the dashboard with state-of-the-art databases like Druid. All to give end users the insights they need. The team provides KPIs by APIs in milliseconds and supports adjoe’s partners by offering the highest availability without outages. This is thanks to working with proper code quality, different testing standards, different mechanisms to auto-scale, and redundant cloud infrastructure. 

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.
What You Will Do
  • You will collect test cases and do manual testing of backend and frontend features in our Advertising dashboard. Test APIs via Postman and/or UI.
  • You will work in cross-functional teams to achieve the highest level of quality across the software development lifecycle.
  • You will collaborate with Product & Tech leads, and other QAs in the team to develop and execute comprehensive test strategies and plans.
  • You will collaborate with BE & FE developers to deliver the best quality products.
  • You will play a key role in reporting and troubleshooting any bugs or errors that may occur.
  • You will take ownership of post-release and post-implementation testing. 
  • You will 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 at least 3 years of proven experience as a quality assurance engineer or similar.
  • You are experienced in QA methodology.
  • You are familiar with Agile frameworks and regression/smoke/LIT testing.
  • You can document and troubleshoot errors.
  • You have a working knowledge of test management software (e.g. qTest, Zephyr) and SQL.
  • You have excellent communication skills.
  • You pay attention to detail.
  • You have an analytical mind and problem-solving attitude.
  • You have strong organizational skills.
  • You can develop a good understanding of the business and can adjust your testing strategy according to this.
  • 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