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.

Technical Product Lead (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 theapplike 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: Core & Integrations 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 Core & Integrations team is responsible for core logic of our advertising platform: campaign distribution to find the best combination of advertising campaigns for end users, campaign management API to optimize existing advertising campaigns and some other smaller API products.

The Core & Integrations team also handles integrations with our external partners like our attribution partners – MMPs (Appsflyer, Adjust, etc.) or aggregators working with the advertisers using our platform.
What You Will Do:
  • Strategy Development: Define and refine the product strategy and roadmap for Campaign distribution, Campaign management, reporting API products based on user feedback, and business goals.
  • Managing new integrations: Implement new and adjust existing integrations with our partners.
  • User Research and Feedback: Conduct user research and gather feedback to understand the needs, behaviors, and pain points of users, facilitating the design of a user-centric product.
  • Product Development Oversight: Work closely with the development team to ensure features are implemented according to specifications and that the product aligns with overall business objectives.
  • Launch and Rollout: Plan and manage the launch of new features and updates, including coordinating with marketing for go-to-market strategies and ensuring that support teams are prepared.
  • Performance Monitoring: Monitor the product’s performance through metrics and KPIs to gauge success and identify areas for improvement.
  • Stakeholder Communication: Act as the primary liaison between the Core & Integrations team and other business & tech units and between the team and external integration partners (MMPs, agencies, direct advertisers, etc.). Regularly communicate progress, risks, and opportunities to all stakeholders, including senior management, to ensure alignment and support.
  • Who You Are:
  • You have 5+ years’ of experience working as Product Lead / Manager / Owner – preferably in B2B SaaS products, developing web or mobile applications.
  • You like running new experiments, a/b testing and know how to do it properly.
  • You have a proven track record of delivering data-driven products and solutions that have contributed to business growth and scalability and make decisions based on date.
  • You have experience in hiring people, doing regular 1-1s, creating career plans.
  • You can speak both tech & business languages: discuss feature implementation with engineers and business needs with business development colleagues. You understand how API and databases work and can explain it.
  • You have experience in basic data analysis and extracting valuable insights from data.
  • You have experience in working with task-tracking tools (Jira, GitLab, etc).
  • Plus: You have experience in using BI tools (QuickSight, Tableau, MS PowerBI).
  • Plus: You know how to work with SQL, Python, or R.
  • Plus: You experience managing DS teams.
  • Plus: You have technical background.
  • Plus: You have AdTech experience.
  • Have You 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 personal trainer, various classes like Yoga with expert teachers.
  • Activity Package: Regular team and company events, hackathons.
  • Education Package: Opportunities to boost your professional development with courses and trainings directly connected to your career goals 
  • Wealth building: virtual stock options for all our regular employees.
  • Free of charge access to our EAP (Employee Assistance Program) which is a counseling service designed to support your mental health and well-being.
  • 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