Skip to content

Edvins Antonovs

Exploring TypeScript: generics (w/ React example)

TypeScript generics is a topic I needed help grasping for a long time. I decided to learn about it and write a blog post to understand it better. Besides understanding generics, we will see their usage with a real-world example involving a React custom useFetch hook.


tl;dr

  • TypeScript generics are a powerful tool that can significantly enhance your code's flexibility and type safety.
  • By leveraging generics, and you can create reusable components and functions that work seamlessly with different data types.
  • The React custom useFetch hook example showcased how generics can help you handle diverse API responses while maintaining type correctness.
  • As your projects evolve, incorporating generics into your toolkit will prove invaluable.

Understanding Generics

Generics in TypeScript provide a way to write functions, classes, and interfaces that work with various data types while maintaining type information. This allows you to create more flexible and reusable components that support type safety. Generics are indicated by angle brackets <T> following a function or type declaration.

Let's start with a basic example using a generic function:

1// Generic function to print elements of an array
2function printArrayElements<T>(arr: T[]): void {
3 arr.forEach((item) => console.log(item));
4}
5
6const stringArray: string[] = ['apple', 'banana', 'cherry'];
7const numberArray: number[] = [1, 2, 3];
8
9printArrayElements(stringArray);
10printArrayElements(numberArray);

Here, the printArrayElements function can handle both arrays of strings and numbers, ensuring type safety.


React custom useFetch hook with generics

Let's dive into a practical scenario where generics shine: creating a dynamic type for a React useFetch hook. This hook fetches data from an API and provides the fetched data, error information, and loading state to the component. Generics will help us handle different response types from various endpoints.

1import { useState, useEffect } from 'react';
2
3// Define a generic type for the response data
4interface ApiResponse<T> {
5 data: T | null;
6 error: Error | null;
7 loading: boolean;
8}
9
10// Define a custom hook that fetches data
11function useFetch<T>(url: string): ApiResponse<T> {
12 const [data, setData] = useState<T | null>(null);
13 const [error, setError] = useState<Error | null>(null);
14 const [loading, setLoading] = useState(true);
15
16 useEffect(() => {
17 async function fetchData() {
18 try {
19 const response = await fetch(url);
20 const responseData = await response.json();
21 setData(responseData);
22 } catch (err) {
23 setError(err);
24 } finally {
25 setLoading(false);
26 }
27 }
28
29 fetchData();
30 }, [url]);
31
32 return { data, error, loading };
33}
34
35// Usage in a component
36function App() {
37 const apiUrl = 'https://api.example.com/data'; // Replace with your API URL
38
39 const { data, error, loading } = useFetch<{ message: string }>(apiUrl);
40
41 if (loading) {
42 return <p>Loading...</p>;
43 }
44
45 if (error) {
46 return <p>Error: {error.message}</p>;
47 }
48
49 return (
50 <div>
51 <h1>API Data</h1>
52 <p>{data?.message}</p>
53 </div>
54 );
55}
56
57export default App;

Best use cases for generics

Generics offer a versatile solution for various scenarios:

  • Collections. Build data structures that hold multiple types while retaining type safety.
  • Functions and APIs. Create versatile functions that handle various input and output types.
  • Custom data structures. Design classes/interfaces to take different data types without compromising type checking.
  • Frameworks and libraries. Craft flexible APIs in frameworks and libraries to cater to diverse user needs.
© 2024 by Edvins Antonovs. All rights reserved.