close
close

first Drop

Com TW NOw News 2024

Mastering React Components with TypeScript Generics
news

Mastering React Components with TypeScript Generics

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

TypeScript has made it much easier to maintain large codebases with thousands of lines of code and to create features that are more user-friendly and reusable for developers.

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 to your code to build highly reusable and type-safe components.

Generics in TypeScript are, in simple terms, constructs that allow you to abstract types, making your code more reusable and extensible. Generics are especially useful when you are not sure what specific type can be passed into your function. A common use case for generics is to improve the reusability of functions.

The syntax for generic types is as follows:

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

printstring>("Hello"); // This works fine because we're specifying 'string' as the type.
printstring>({ message: "Hello" }); // TypeScript error: expected a 'string'.
Go to full screen mode

Exit full screen

In this example the Type generic can be anything, and we are not interested in what the specific type is. The developer using this feature can specify what type to use.

In the React ecosystem, generics are used frequently, although they are not always visible. Generics are implemented in almost every library because libraries are designed to be reusable. For example, when you call functions like axios.get(fetchUsers) or use lodash _.groupBy(users, 'role')These features use generic under-the-hood features.

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 context, which we’ll explore next.

What if I told you that if you work with TypeScript and React, you’re already working with generics every day — even if you don’t realize it? Think of hooks as useMemo or useCallback.

You may 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.
Go to full screen mode

Exit full screen

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

This concept in TypeScript is known as type inference. Type inference in TypeScript is a feature where the compiler automatically discovers the type of a variable or expression without you explicitly specifying it. Simply put, TypeScript “compiles” a portion of your code and, using typeof and other constructions, can understand what type you have assigned.

Here’s a simple example:

let message = "Hello, TypeScript!"; // type of message is string
Go to full screen mode

Exit full screen

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

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

print("Hello"); // Works
print({ message: "Hello" }); // Also works
Go to full screen mode

Exit full screen

This brief introduction to type inference lays the foundation for effectively using generic types in our React code.

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

Image description

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

type Agent = {
    firstName: string
    secondName: string
}

type Merchant = {
    companyName: string
    companyId: number
}
Go to full screen mode

Exit full screen

These are two different types that share a common property: 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 element:

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>
    )
}
Go to full screen mode

Exit full screen

At first glance, the above code looks fine, but it will not work well in TypeScript because TypeScript cannot infer the type of the code. e in the onChange function. To annotate it correctly, 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: SyntheticEventHTMLInputElement>) => {
    const newSearch = e.currentTarget.value;

    setSearch(newSearch);
  };
Go to full screen mode

Exit full screen

Add list and more properties

The next step is to add a list of items. For now, we simplify the solution and pass an array of strings for this component:

type Props = {
  items: string();
};

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

  const onChange = (e: SyntheticEventHTMLInputElement>) => {
    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>
  );
};
Go to full screen mode

Exit full screen

After this we can add some basic searches. The first step is 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: SyntheticEventHTMLInputElement>) => {
    const newSearch = e.currentTarget.value;

    setSearch(newSearch);
    setFilteredItems(
      filteredItems.filter((item) =>
        item.toLocaleLowerCase().includes(newSearch.toLocaleLowerCase())
      )
    );
  };
Go to full screen mode

Exit full screen

Generic Functions and Arrow Functions in React

Before we implement Generics into our code, we need to address a problem with generics. It is not clear how to use it with the arrow function when you first try it.

export const Autosuggestion = Item>({ items }: PropsItem>)  => {}
Go to full screen mode

Exit full screen

The above code will not work in TypeScript due to a limitation in using for generic type parameter declarations combined with JSX grammar.

There are two ways to solve this problem:

// Extend from object or unknown
export const Autosuggestion = Item extends Object>({ items }: PropsItem>) => {}
Go to full screen mode

Exit full screen

// Put comma after Generic and typescript can't understand that this is ts annotation and not JSX
export const Autosuggestion = Item,>({ items }: PropsItem>) => {}
Go to full screen mode

Exit full screen

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

Component made reusable with Generics

With our knowledge of Generics we can make this part more reusable:

// First pass generics to your type
type PropsItem> = {
  items: Item();
};

export const Autosuggestion = Item,>({ items }: PropsItem>) => {
     // Remember type infering? This is why you don't need to change anything here
    const (filteredItems, setFilteredItems) = useState(items);
}
Go to full screen mode

Exit full screen

Next, we need to decide on our filtering strategy for items in the component. There are many different solutions, but let’s say we want to pass the filter function and render function to make this component more reusable and to leverage more power of Generics.

type PropsItem> = {
  items: Item();
  filterFn: (item: Item, search: string) => Boolean;
};

export const Autosuggestion = Item,>({ items, filterFn }: PropsItem>) => {
  const onChange = (e: SyntheticEventHTMLInputElement>) => {
    const newSearch = e.currentTarget.value;

    setSearch(newSearch);
    setFilteredItems(filteredItems.filter((item) => filterFn(item, search)));
  };
}
Go to full screen mode

Exit full screen

Next, we want to display the result. Since we don’t know the type of the item in advance, we can also move this logic to the parent. In React, this is a common pattern called “display item“when the parent provides the function of how to render a specific item. This is the implementation:

type PropsItem> = {
  items: Item();
  filterFn: (item: Item, search: string) => Boolean;
  renderItem: (item: Item) => ReactNode;
};

export const Autosuggestion = Item,>({ items, renderItem }: PropsItem>) => {
    const (filteredItems, setFilteredItems) = useState(items);

    return (
      {filteredItems.length > 0 && (
        ul>
          {filteredItems.map((item) => (
            li>{renderItem(item)}li>
          ))}
        ul>
      )}
    )
}
Go to full screen mode

Exit full screen

Finish the part

Let’s take stock again:

type PropsItem> = {
  items: Item();
  filterFn: (item: Item, search: string) => Boolean;
  renderItem: (item: Item) => ReactNode;
};

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

  const onChange = (e: SyntheticEventHTMLInputElement>) => {
    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>
  );
};
Go to full screen mode

Exit full screen

An 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>
    );
}
Go to full screen mode

Exit full screen

If you said we didn’t provide Generics ourselves. Typescript derives all the types for us by matching items={agents} of type Props = { items: Item(); } and add corresponding types for filterFn And renderItemHerein lies the true power of generic drugs.

As you can see, the component itself is reusable and can be extended with different functionalities.

The full code can be found in the Sandbox.

In this article, we explored the power of TypeScript generics, what type inferring in Typescript is, and how it relates to Generics. We also built our reusable component based on generics.

We’ve seen how TypeScript’s type inference goes 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 utility applications or complex UI components, generics provide a way to ensure your code is both scalable and easy to understand.