Mastering React Components with TypeScript Generics

Ulad Ramanovich - Aug 23 - - Dev Community

Typescript in react ecosystem

In recent years, TypeScript has become an essential part of the React ecosystem. Not too long ago, almost every package required an additional package just for type definitions, like @types/react-select, and those types were sometimes incorrect. In many projects, there were numerous if/else checks just to ensure a property existed before using it, to avoid errors like "Cannot read properties of undefined."

With TypeScript, it has become much easier to maintain large codebases with thousands of lines of code and to create more developer-friendly, reusable functions.

In this article, we’ll discuss one of the most underutilized yet powerful concepts in TypeScript — generics — specifically within the context of React. We’ll explore how you can apply generics in your code to build highly reusable and type-safe components.

What is generics in typescript

Generics in typescript, in simple words, are constructs that allow you to abstract type, making your code more reusable and extensible. Generics are particularly useful when you’re unsure of what specific type might be passed into your function. A common use case for generics is to enhance the reusability of functions.

The syntax for generics looks like this:

function print<Type>(data: Type) {
 console.log(data)
}

print<string>("Hello"); // This works fine because we're specifying 'string' as the type.
print<string>({ message: "Hello" }); // TypeScript error: expected a 'string'.
Enter fullscreen mode Exit fullscreen mode

In this example, the Type generic can be anything, and we're not concerned with what the specific type is. The developer using this function can specify which type to use.

In the React ecosystem, generics are commonly used, though they may not always be visible. Generics are implemented in almost every library because libraries are designed to be reusable. For instance, when you call functions like axios.get(fetchUsers) or use lodash _.groupBy(users, 'role'), these functions are using generics under the hood.

But why don’t you always need to explicitly specify the generic type? This is an important TypeScript concept to understand before diving into React: TypeScript can often infer the type based on the context, which we’ll explore next.

Typescript types inferring magic

What if I told you that if you’re working with TypeScript and React, you’re already working with generics daily — even if you’re not aware of it? Consider hooks like useMemo or useCallback.

You might have a line like this in your code:

const aLotOfData = []; // imagine thousands of elements here

const memoizedHugeArray = useMemo(() => {
  return sortingOfHugeArray(aLotOfData);
}, [aLotOfData]);

memoizedHugeArray.length; // Works! TypeScript knows this is an array.
Enter fullscreen mode Exit fullscreen mode

You might think that Typescript is smart enough to understand types just by reading your code, but in reality, even you can’t understand your own code after a while. So, how typescript can do this?

This concept in TypeScript is known as type inference. Type inference in TypeScript is a feature where the compiler automatically finds out the type of a variable or expression without you explicitly specifying it. In simple terms, TypeScript "compiles" part of your code and, using typeof and other constructs, can understand what type you have assigned.

Here’s a simple example:

let message = "Hello, TypeScript!"; // type of message is string
Enter fullscreen mode Exit fullscreen mode

The same principle applies to generics. TypeScript can infer the type for generics automatically:

function print<Type>(data: Type) {
  console.log(data);
}

print("Hello"); // Works
print({ message: "Hello" }); // Also works
Enter fullscreen mode Exit fullscreen mode

This brief introduction to type inference sets the stage for understanding how to use generics effectively in our React code.

Where to use generics in react

Imagine we have a task to create an Autosuggestion component where you can pass a list of agents or merchants and filter them by a specific property as you type in the input field. Here is UI example:

Image description

This might sound like a simple task, but with TypeScript, it becomes more challenging. Here are the types of agents and merchants:

type Agent = {
    firstName: string
    secondName: string
}

type Merchant = {
    companyName: string
    companyId: number
}
Enter fullscreen mode Exit fullscreen mode

These are two different types that share a common trait: they are both objects. This is a perfect scenario where TypeScript and generics can help us solve the problem and create a single, reusable component.

Basic component

Let’s start with a basic annotation of our Autosuggestion component:

type Props = {

}
const Autosuggestion = ({}: Props) => {
    const [search, setSearch] = useState("")

    const onChange = (e) => {
        const newSearch = e.currentTarget.value

        setSearch(newSearch)
    }

    return (
        <div>
            <input value={search} onChange={onChange} />
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

At first glance, the code above might seem fine, but it won’t work correctly in TypeScript because TypeScript cannot infer the type of e in the onChange function. To properly annotate it, we need to use Generic from React SyntheticEvent (a React abstraction for most events in JSX) along with the predefined TypeScript type HTMLInputElement for input DOM elements. This tells TypeScript that we expect a "synthetic React event" for an "input element."

  const onChange = (e: SyntheticEvent<HTMLInputElement>) => {
    const newSearch = e.currentTarget.value;

    setSearch(newSearch);
  };
Enter fullscreen mode Exit fullscreen mode

Adding list and more properties

As our next step let’s add a list with items. For now, we simplify the solution and pass an array of string for this component:

type Props = {
  items: string[];
};

export const Autosuggestion = ({ items }: Props) => {
  const [search, setSearch] = useState("");

  const onChange = (e: SyntheticEvent<HTMLInputElement>) => {
    const newSearch = e.currentTarget.value;

    setSearch(newSearch);
  };

  return (
    <div>
      <input value={search} onChange={onChange} />

      {items.length > 0 && (
        <ul>
          {items.map((item) => (
            <li>{item}</li>
          ))}
        </ul>
      )}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

After this, we can add some basic search. As a first step, we need to copy the items list and filter it by text (and add toLocaleLowerCase for a better experience):

  const [filteredItems, setFilteredItems] = useState(items);

  const onChange = (e: SyntheticEvent<HTMLInputElement>) => {
    const newSearch = e.currentTarget.value;

    setSearch(newSearch);
    setFilteredItems(
      filteredItems.filter((item) =>
        item.toLocaleLowerCase().includes(newSearch.toLocaleLowerCase())
      )
    );
  };
Enter fullscreen mode Exit fullscreen mode

Generics and arrow functions in react

Before we bring Generics to our code we should talk about one problem with generics. It’s not obvious how to use it with arrow function when you try first time.

export const Autosuggestion = <Item>({ items }: Props<Item>)  => {}
Enter fullscreen mode Exit fullscreen mode

The code above doesn’t work in typescript because of a limitation in the use of <Item> for generic type parameter declarations combined with JSX grammar.

There are two ways to solve this issue:

// Extend from object or unknown
export const Autosuggestion = <Item extends Object>({ items }: Props<Item>) => {}
Enter fullscreen mode Exit fullscreen mode
// Put comma after Generic and typescript can't understand that this is ts annotation and not JSX
export const Autosuggestion = <Item,>({ items }: Props<Item>) => {}
Enter fullscreen mode Exit fullscreen mode

I prefer the second solution as it is easier to understand. However, you can use any option that fits your case.

Made component reusable with Generics

With our knowledge of Generics let’s make this component more reusable:

// First pass generics to your type
type Props<Item> = {
  items: Item[];
};

export const Autosuggestion = <Item,>({ items }: Props<Item>) => {
     // Remember type infering? This is why you don't need to change anything here
    const [filteredItems, setFilteredItems] = useState(items);
}
Enter fullscreen mode Exit fullscreen mode

After this, we should decide our filtering strategy for items in the component. There are plenty of different solutions but let's say we want to pass the filter function and render function to make this component more reusable and utilize more power of Generics.

type Props<Item> = {
  items: Item[];
  filterFn: (item: Item, search: string) => Boolean;
};

export const Autosuggestion = <Item,>({ items, filterFn }: Props<Item>) => {
  const onChange = (e: SyntheticEvent<HTMLInputElement>) => {
    const newSearch = e.currentTarget.value;

    setSearch(newSearch);
    setFilteredItems(filteredItems.filter((item) => filterFn(item, search)));
  };
}
Enter fullscreen mode Exit fullscreen mode

After this, we want to display the result. As we don’t know the type of the item in advance we can move this logic to the parent too. In React this is a common pattern named “render item” when the parent provides the function of how to render a specific item. Here is the implementation:

type Props<Item> = {
  items: Item[];
  filterFn: (item: Item, search: string) => Boolean;
  renderItem: (item: Item) => ReactNode;
};

export const Autosuggestion = <Item,>({ items, renderItem }: Props<Item>) => {
    const [filteredItems, setFilteredItems] = useState(items);

    return (
      {filteredItems.length > 0 && (
        <ul>
          {filteredItems.map((item) => (
            <li>{renderItem(item)}</li>
          ))}
        </ul>
      )}
    )
}
Enter fullscreen mode Exit fullscreen mode

Finalise the component

Let's put everything together:

type Props<Item> = {
  items: Item[];
  filterFn: (item: Item, search: string) => Boolean;
  renderItem: (item: Item) => ReactNode;
};

export const Autosuggestion = <Item,>({
  items,
  filterFn,
  renderItem,
}: Props<Item>) => {
  const [search, setSearch] = useState("");
  const [filteredItems, setFilteredItems] = useState(items);

  const onChange = (e: SyntheticEvent<HTMLInputElement>) => {
    const newSearch = e.currentTarget.value;

    setSearch(newSearch);
    setFilteredItems(filteredItems.filter((item) => filterFn(item, search)));
  };

  return (
    <div>
      <input value={search} onChange={onChange} />

      {filteredItems.length > 0 && (
        <ul>
          {filteredItems.map((item) => (
            <li>{renderItem(item)}</li>
          ))}
        </ul>
      )}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

And example of usage:

type Agent = {
  firstName: string;
  lastName: string;
};

const agents: Agent[] = [
  {
    firstName: "Ethan",
    lastName: "Collins",
  },
  {
    firstName: "Sophia",
    lastName: "Ramirez",
  },
  {
    firstName: "Liam",
    lastName: "Carter",
  },
];

export default function App() {
    return (
        <div className="App">
          <Autosuggestion
            items={agents}
            filterFn={(agent, search) => agent.firstName.includes(search)}
            renderItem={(agent) => (
              <div>
                {agent.firstName} {agent.lastName}
              </div>
            )}
          />
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

If you mentioned we didn't provided any Generics itself. Typescript infer all types just for us by the matching items={agents} with type Props<Item> = { items: Item[]; } and add corresponding types for filterFn and renderItem. This is where true power of Generics.

As you can see component itself reusable and could be extended with various of functionality.

Full code you can find in the Sandbox.

Conclusion

In this article, we’ve explored the power of TypeScript generics, what is type inferring in Typescript, and how this is connected with Generics and build our reusable component based on generics.

We’ve seen how TypeScript’s type inference works hand-in-hand with generics to automatically determine types, making your code more robust without the need for excessive type annotations. Whether you’re building simple utilities or complex UI components, generics offer a way to ensure your code is both scalable and easy to understand.

. . . . . .
Terabox Video Player