DEV Community

Sergey Egorov
Sergey Egorov

Posted on

TypeScript Generics in React Components.

Generics are not something you use in React components every day. However, in certain cases the allow you to write components that are both flexible and type-safe.

Let's consider a few common approaches to describe a component with TypeScript

// Just FC component with no props
const Component = () => (<div>Hey ya!!!</div>)
Enter fullscreen mode Exit fullscreen mode

Another one:

// FC component with props
interface Props{
  name: string;
}

const Component: React.FC<Props> = ({ name })=>(<div>Hey {name} ya!!!</div>)
Enter fullscreen mode Exit fullscreen mode

For some time now React.FC is not a perfect way to describe a React component. The motivation is React.FC does not support generics in the way we might need, however, in my view it's not a reason. How often do you really need generics in your components? Hardly ever. In my own practice I continue using React.FC due to readability.

Another way to describe a functional component with no React.FC is just to specify the props type directly

// FC component with simple props
interface Props{
  name: string;
}

const Component = ({ name }:Props)=>(<div>Hey {name} ya!!!</div>)
Enter fullscreen mode Exit fullscreen mode

Now nothing stops us from passing a generic type into props, but the cases above do not require any generics to pass. Let's consider a case when it is really needed.

Here is a situation when we have a DTO with an array received from an API. There are many properties inside. One of them is a property with the value that we need. Our component does not know the name of that property which should be fairly common. We can use a common approach for that with a callback function that must return a name, however, it would be great if, inside that callback, we know that type of data that was provided into the component.

Simple example. We have a ResponseDTO type

interface ResponseDTO{
  id: number;
  name: string;
}
Enter fullscreen mode Exit fullscreen mode

and a component that works with this specific type of DTO

interface ExampleProps {
  data: ResponseDTO[];
  getName: (item: ResponseDTO) => string;
}

export const Example = ({
  data,
  getName = (item: ResponseDTO) => item.name,
}: ExampleProps) => {
  return (
    <div>
      {data.map((item) => (
        <div key={getName(item)}>{getName(item)}</div>
      ))}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Look at getName property here. In this case it's not necessary because the interface is explicit. Our component knows everything about the data structure. And we can safely use the name property from our DTO.

Now imagine. We have many different DTOs with different parameters and one of those can be considered as a name. Here generics open the gates of opportunities for us.

First we need to update our Props interface

interface Props<T> {
  data: T[];
  getName: (item: T) => string;
}
Enter fullscreen mode Exit fullscreen mode

Why Props<T> needs the generic parameter? The type T must be declared on the Props interface because this is where the relationship between the incoming data and the callback is defined. Without this TS would have no way to ensure that the items inside data and the parameter of getName refer to the same type.

This way we say that we do not know which data will come to our component but it's not any. Now let's look at our component.

export const NameViewer = <T,>({ data, getName }: Props<T>) => {
  return (
    <>
      Look at the names:
      {data.map((item) => (
        <div key={getName(item)}>{getName(item)}</div>
      ))}
    </>
  );
};

Enter fullscreen mode Exit fullscreen mode

Nothing special, right? We just iterate over an array unknown to this component and display values considered as names.

Note: In .tsx files, generic parameters in arrow functions may conflict with JSX syntax. Writing <T,> (with a trailing comma) disambiguates the syntax and allows TypeScript to correctly parse it as a generic declaration rather than a JSX tag.

Let's try to use this component.

const unknownTypeData = [
  { id: 1, nameProp: 'John' },
  { id: 2, nameProp: 'Jane' },
];

const anotherUnknownTypeData = ['Foo', 'Bar'];

export const BusinessComponent = () => {
  return (
    <div>
      <NameViewer
        data={unknownTypeData}
        getName={(item) => item.nameProp}
      />
      <NameViewer
        data={anotherUnknownTypeData}
        getName={(item) => item}
      />
    </div>
  );
};

Enter fullscreen mode Exit fullscreen mode

NameViewer now can accept any types of data that you might want to iterate.

I have just shown you a very simple but compact and useful example. It's only a small drop in the sea of generics. My next article will extend your knowledge with conditional generics that are also very useful. See you.

Top comments (0)