Typescript generics for React developers

Intro

Introducing: Judi 👩🏽‍💻. Judi is a very ambitious developer and wants to implement her own online shop, a competitor to Amazon. She will sell everything there: books, movies, more than a thousand types of various categories of goods. And now she’s at the stage she needs to implement a page with a bunch of identical-looking selects for multiple categories of goods on the page.

import React from 'react';type SelectOption = {
value: string;
label: string;
};
type SelectProps = {
options: SelectOption[];
onChange: (value: string) => void;
};
export const Select = ({ options, onChange }: SelectProps) => {
return (
<select onChange={(e) => onChange(e.target.value)}>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);
};
<>
<Select option={bookOptions} onChange={(bookId) => doSomethingWithBooks(bookId)} />
<Select option={movieOptions} onChange={(movieId) => doSomethingWithMovies(movieId)} />
</>
  1. onChange handler returns only the id of the changed value, so she needed to manually filter through arrays of data every time she needed to find the actual value that has changed
  2. it’s completely not typesafe, and very easy to make a mistake. Once she used doSomethingWithBooks handler on a select with moviesOptions by mistake, and that blew up the entire page and caused an incident. Customers were not happy 😞

💪 Time to refactor

Judi wanted to significantly improve her application and:

  • remove all the code that was generating the select options everywhere
  • make the select component type-safe, so that next time she uses the wrong handler with a set of options, the type system could catch it
  • onChange handler returns the "raw" typed value, not just its id, hence removing the need to manually search for it on the consumer side
  • options and onChange values should be connected; so that if she uses doSomethingWithBooks on a select that accepted movies as value, it would've been caught by the type system.
export type Book = {
id: string;
title: string;
author: string; // only books have it
};
export type Movie = {
id: string;
title: string;
releaseDate: string; // only movies have it
};
... // all other types for the shop goods

Strongly typed select — first attempt

Judi, again, started simple: she decided that she’ll implement a select that accepts only books for now, and then just modify it to accept the rest of the types afterwards.

type BookSelectProps = {
values: Book[];
onChange: (value: Book) => void;
};
export const BookSelect = ({ values, onChange }: BookSelectProps) => {
const onSelectChange = (e) => {
const val = values.find((value) => value.id === e.target.value);
if (val) onChange(val);
};
return (
<select onChange={onSelectChange}>
{values.map((value) => (
<option key={value.id} value={value.id}>
{value.title}
</option>
))}
</select>
);
};

Strongly typed select — actual solution with typescript generics

And this is finally where typescript generic types could come in handy. Generics, in a nutshell, are nothing more than a placeholder for a type. It’s a way to tell typescript: I know I will have a type here, but I have no idea what it should be yet, I’ll tell you later. The simplest example of a generic, used in the documentation, is this:

function identity<Type>(a: Type): Type {
return a;
}
const a = identity<string>("I'm a string") // "a" will be a "string" type
const b = identity<boolean>(false) // "b" will be a "boolean" type
const a = identity<string>(false) // typescript will error here, "a" can't be boolean
const b = identity<boolean>("I'm a string") // typescript will error here, "b" can't be string
type Base = {
id: string;
title: string;
};
type GenericSelectProps<TValue> = {
values: TValue[];
onChange: (value: TValue) => void;
};
export const GenericSelect = <TValue extends Base>({ values, onChange }: GenericSelectProps<TValue>) => {
const onSelectChange = (e) => {
const val = values.find((value) => value.id === e.target.value);
if (val) onChange(val);
};
return (
<select onChange={onSelectChange}>
{values.map((value) => (
<option key={value.id} value={value.id}>
{value.title}
</option>
))}
</select>
);
};
// This select is a "Book" type, so the value will be "Book" and only "Book"
<GenericSelect<Book> onChange={(value) => console.log(value.author)} values={books} />
// This select is a "Movie" type, so the value will be "Movie" and only "Movie"
<GenericSelect<Movie> onChange={(value) => console.log(value.releaseDate)} values={movies} />

Typescript generics in React hooks bonus

Did you know that most React hooks are generics as well? You can explicitly type things like useState or useReducer and avoid unfortunate copy-paste driven development mistakes, where you define const [book, setBook] = useState(); and then pass a movie value there by accident. Things like that could cause a little crash of reality for the next person who reads the code and sees setBook(movie) during the next refactoring.

export const AmazonCloneWithState = () => {
const [book, setBook] = useState();
const [movie, setMovie] = useState();
return (
<>
<GenericSelect<Book> onChange={(value) => setMovie(value)} values={booksValues} />
<GenericSelect<Movie> onChange={(value) => setBook(value)} values={moviesValues} />
</>
);
};
export const AmazonCloneWithState = () => {
const [book, setBook] = useState<Book | undefined>(undefined);
const [movie, setMovie] = useState<Movie | undefined>(undefined);
return (
<>
<GenericSelect<Book> onChange={(value) => setBook(value)} values={booksValues} />
<GenericSelect<Movie> onChange={(value) => setMovie(value)} values={moviesValues} />
</>
);
};

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Nadia Makarevich

Nadia Makarevich

768 Followers

Frontend architect, coder. Love solving problems, fixing things and writing in-depth tech articles: https://www.developerway.com