Typescript generics for React developers
Tags: webdev, react, typescript, tutorial
Published: 2021-11-25 18:17:23


Originally published at [https://www.developerway.com](https://www.developerway.com). The website has more articles like this 😉

---

I don’t know about you, but I fall asleep every time I’m trying to read typescript documentation. There is something in the way it is written that signals to my brain that I should not even attempt to comprehend it until I had a good night’s sleep, three coffees, and ideally some chocolate to stimulate the brain cells. I think I now found my purpose for the next few months: I want to re-write typescript documentation in a way that is actually understandable by a casual reader 😊

Let's start with one of the pain points many developers are struggling with: generics! And we’re going to start with a bottom-up approach: let’s implement a component without generics, and introduce them only when we need them.

## 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.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iola2nlx52asw8xtsb8t.png)

She starts very simple: a select component, that accepts an array of options with `value` and `title` to render those, and an `onChange` handler so that she can do something when a value in a select is changed (every select will do different things!).

```ts
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>
  );
};
```

This seems like an okay solution for the purpose: she can re-use those selects for all her products and take over the online shopping world.

```ts
<>
  <Select option={bookOptions} onChange={(bookId) => doSomethingWithBooks(bookId)} />
  <Select option={movieOptions} onChange={(movieId) => doSomethingWithMovies(movieId)} />
</>
```

Unfortunately, as the shop grew, she found a few problems with this solution:

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

Judi wanted to significantly improve her application and:

- 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

She decided, that what she needs is a select component that:

- 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.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mq3mern80tqgpw3knls7.png)

She already had all her data typed, so only the select component needed some work.

```ts
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.

```ts
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>
  );
};
```

This looked great already: now she doesn’t need to worry about mixing handlers or values up, this select accepts only Books are properties and always returns a Book when a value is changed.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gkgyhpwvsjz355ve18il.png)

Now, all she needs to do is turn `BookSelect` into `GenericSelect` and teach it how to deal with the rest of the data in the app. First, she just tried to do a <a href="https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html" target="_blank" rel="noreferrer">union type</a> on the values (if you’re not familiar with those - it’s just a fancy word for `or` operator for types)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bsp953e28y0dmj3ueevn.png)

But it was almost instantly obvious to her, that this is not a very good idea. Not only because she’d have to manually list _**all**_ supported data types in the select and change it every single time a new data type is added. But it actually made things worst from the code complexity perspective: typescript _doesn’t actually know_ what exactly is passed in the `onChange` callback with this approach, regardless of what goes into the `values`. So even the most obvious and simple use case of logging the author of the selected book will make typescript super confused:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3j1838ug7axkxlpziun9.png)

t knows, that in value there can be either `Book` or `Movie`, but it doesn’t know what _exactly_ is there. And since `Movie` doesn’t have an author field, typescript will consider the code above an error.

See <a href="https://codesandbox.io/s/bold-germain-ohux0?file=/src/App.tsx" target="_blank" rel="noreferrer">example of this error in codesandbox.</a>

## Strongly typed select - actual solution with typescript generics

And this is finally where typescript <a href="https://www.typescriptlang.org/docs/handbook/2/generics.html"  target="_blank" rel="noreferrer">generic types</a> 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:

```ts
function identity<Type>(a: Type): Type {
  return a;
}
```

which translates roughly into: “I want to define a function that accepts an argument of _**some**_ type and returns a value of exactly the same type. And I will tell you later which type it is.”

And then later in the code, you can just tell this function what _exactly_ you meant by this placeholder type:

```ts
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
```

And then any attempt to mistype it will fail:

```ts
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
```

So the way to apply this to the select component is this:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5pit4xicpo7midoifo7l.png)

Now, I intentionally don’t include code in a copy-pasteable form here, because this example is actually not going to work 😅. The first reason is very _React in Typescript_ specific: since this is a React component, typescript will assume that the very first `<Tvalue>` is a `jsx` element and will fail. The second reason is exclusively generics problem: when we try to access `value.title` or `value.id` in our select, typescript at this point _**still doesn’t know**_ which type we have in mind for this value. It has no idea which properties our value can have and rightfully so. Why would it?

This leads us to the last piece of this puzzle: generic **constraints**.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1goqw0ozqpqqkxwfhzp5.png)

Constraints are used to narrow down the generic type so that typescript can make at least some assumptions about `TValue`. Basically, it’s a way to tell typescript: I have no idea what `TValue` should be yet, but I know for a fact that it will always have _**at least**_ `id` and `title`, so you’re free to assume they will be there.

And now the select component is complete and fully functional! 💥 🎉 Check it out:

```ts
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>
  );
};
```

And Judi finally can use it to implement all the selects that she wants for her Amazon competitor:

```ts
// 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} />
```

Check out the <a href="https://codesandbox.io/s/relaxed-yalow-p10ee?file=/src/App.tsx" target="_blank" rel="noreferrer">fully working example in codesandbox.</a>

## 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.

This will work fine, although will cause a lot of rage and despair for anyone who’s trying to fix a bug with this setup:

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

This will prevent it, and any malicious attempt to use setBook on a value in the second select will be stopped by typescript:

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

That’s all for today, hope you enjoyed the reading and generics are not a mystery anymore! ✌🏼

...

Originally published at [https://www.developerway.com](https://www.developerway.com). The website has more articles like this 😉

[Subscribe to the newsletter](https://www.developerway.com), [connect on LinkedIn](https://www.linkedin.com/in/adevnadia/) or [follow on Twitter](https://twitter.com/adevnadia) to get notified as soon as the next article comes out.