React TypeScript generic components are a powerful way to build reusable, type-safe UI elements.

Here’s a look at a generic List component that can render any array of items, with full type safety:

import React from 'react';

interface ListItemProps<T> {
  item: T;
  renderItem: (item: T) => React.ReactNode;
}

function ListItem<T>({ item, renderItem }: ListItemProps<T>) {
  return <>{renderItem(item)}</>;
}

interface ListProps<T> {
  data: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string | number;
}

function List<T>({ data, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {data.map((item) => (
        <ListItem<T>
          key={keyExtractor(item)}
          item={item}
          renderItem={renderItem}
        />
      ))}
    </ul>
  );
}

// Example Usage:
interface User {
  id: number;
  name: string;
}

const users: User[] = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
];

const App: React.FC = () => {
  return (
    <List<User>
      data={users}
      renderItem={(user) => <div>{user.name}</div>}
      keyExtractor={(user) => user.id}
    />
  );
};

export default App;

This List component, when used with specific data types like User, provides compile-time guarantees that you’re accessing properties correctly. If you tried to access user.age in renderItem, TypeScript would flag it as an error.

The core problem this pattern solves is the need for highly reusable UI components that can adapt to different data structures without sacrificing type safety. Before generics, you’d often resort to any or complex union types, losing the benefits of TypeScript’s static analysis. Generics allow you to define a component’s structure once and then let TypeScript infer or explicitly set the types for the data it will operate on.

Internally, the <T> in ListProps<T> and ListItemProps<T> declares T as a type parameter. This means T is a placeholder for a specific type that will be provided when the component is used. The data: T[] in ListProps signifies that the data prop will be an array of whatever type T resolves to. Similarly, renderItem: (item: T) => React.ReactNode ensures that the function passed to renderItem expects an argument of type T and returns valid React nodes. The keyExtractor also enforces that it receives an item of type T.

The ListItem component is itself generic, demonstrating how generics can be composed. It takes an item of type T and a renderItem function that knows how to render that specific T. This allows List to delegate the rendering of individual items to ListItem, which is also type-aware.

When you use <List<User> ... />, you are explicitly telling TypeScript that for this instance of the List component, T should be User. This allows TypeScript to check that:

  1. The data prop is indeed an array of User objects.
  2. The renderItem function correctly accepts a User object.
  3. The keyExtractor function correctly accepts a User object and returns a string or number.

The most surprising true thing about using generics in React components is how seamlessly they integrate with the component lifecycle and event handling. You can define generic event handlers that receive typed event objects, or generic state hooks that manage state of a specific generic type, without needing to redefine the component’s core logic.

The next concept you’ll likely encounter is how to handle default type parameters for generic components, allowing them to be used without explicit type arguments in many common scenarios.

Want structured learning?

Take the full React course →