Typescript generics for React developers

Intro

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. the select component accepts options in a very specific format, everything needs to be converted to it by the consumer component. And as the shop grows, more and more pages begin to use it, so that conversion code started to bleed all over the place and became hard to maintain.
  2. 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
  3. 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

  • get rid of all the code that filters through the arrays of raw data here and there
  • 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
  • accepts an array of typed values and transforms it into select options by itself
  • 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

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

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

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} />
</>
);
};

--

--

--

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

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Mutual Exclusion Using RxJS

Advanced typescript for React developers — discriminated unions

js for interview-4

Building a GUI application with Python and Qt

React and Firebase are all you need to host your web apps

My Intro to React-Redux Application

https://youtu.be/T_8TDj2X0OY

React native in-app purchases: simple implementation. Tutorial

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

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

More from Medium

How to Write Performant React Apps with Context

Clean up your React component types 🧼

Add Typescript to React Package

How to use Throttle and Debounce Callbacks in React with Hooks