Mastering React Components with TypeScript Generics
Ulad Ramanovich
Posted on August 23, 2024
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'.
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.
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
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
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:
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
}
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>
)
}
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);
};
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>
);
};
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())
)
);
};
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>) => {}
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>) => {}
// Put comma after Generic and typescript can't understand that this is ts annotation and not JSX
export const Autosuggestion = <Item,>({ items }: Props<Item>) => {}
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);
}
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)));
};
}
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>
)}
)
}
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>
);
};
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>
);
}
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.
Posted on August 23, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.