React & TypeScript: use generics to improve your types
Tags: react, webdev, typescript, tutorial
Published: 2021-10-05 18:32:40

While TypeScript is a godsend for React developers, its syntax is fairly intimidating to newcomers. I think generics are a big part of that: they look weird, their purpose isn't obvious, and they can be quite hard to parse.

This article aims to help you understand and demystify TypeScript generics in general, and their application to React in particular. They aren't that complex: if you understand functions, then generics aren't that far off.

## What are generics in TypeScript?

To understand generics, we'll first start by comparing a standard TypeScript type to a JavaScript object.

```jsx
// a JavaScript object
const user = {
  name: 'John',
  status: 'online',
};

// and its TypeScript type
type User = {
  name: string;
  status: string;
};
```

As you can see, very close. The main difference is that in JavaScript you care about the values of your variables, while in TypeScript you care about the type of your variables.

One thing we can say about our `User` type is that its `status` property is too vague. A status usually has predefined values, let's say in this instance it could be either "online" or "offline". We can modify our type:

```jsx
type User = {
  name: string;
  status: 'online' | 'offline';
};
```

But that assumes we already know the kind of statuses there are. What if we don't, and the actual list of statuses changes? That's where generics come in: **they let you specify a type that can change depending on the usage**.

We'll see how to implement this new type afterward, but for our `User` example using a generic type would look like this:

```jsx
// `User` is now a generic type
const user: User<'online' | 'offline'>;

// we can easily add a new status "idle" if we want
const user: User<'online' | 'offline' | 'idle'>;
```

What the above is saying is "the `user` variable is an object of type `User`, and by the way the status options for this user are either 'online' or 'offline'" (and in the second example you add "idle" to that list).

All right, the syntax with angle brackets `< >` looks a bit weird. I agree. But you get used to it.

Pretty cool right? Now here is how to implement this type:

```jsx
// generic type definition
type User<StatusOptions> = {
  name: string;
  status: StatusOptions;
};
```

`StatusOptions` is called a "type variable" and `User` is said to be a "generic type".

Again, it might look weird to you. But this is really just a function! If I were to write it using a JavaScript-like syntax (**_not_** valid TypeScript), it would look something like this:

```jsx
type User = (StatusOption) => {
  return {
    name: string;
    status: StatusOptions;
  }
}
```

As you can see, it's really just the TypeScript equivalent of functions. And you can do cool stuff with it.

For example imagine our `User` accepted an array of statuses instead of a single status like before. This is still very easy to do with a generic type:

```jsx
// defining the type
type User<StatusOptions> = {
  name: string;
  status: StatusOptions[];
};

// the type usage is still the same
const user: User<'online' | 'offline'>;
```

If you want to learn more about generics, you can check out [TypeScript's guide on them](https://www.typescriptlang.org/docs/handbook/2/generics.html).

### Why generics can be very useful

Now that you know what generic types are and how they work, you might be asking yourself why we need this. Our example above is pretty contrived after all: you could define a type `Status` and use that instead:

```jsx
type Status = 'online' | 'offline';

type User = {
  name: string;
  status: Status;
};
```

That's true in this (fairly simple) example, but there are a lot of situations where you can't do that. It's usually the case when you want to have a shared type used in multiple instances that each has some difference: you want the type to be dynamic and adapt to how it's used.

A very common example is having a function that returns the same type as its argument. The simplest form of this is the identity function, which returns whatever it's given:

```jsx
function identity(arg) {
  return arg;
}
```

Pretty simple right? But how would you type this, if the `arg` argument can be any type? And don't say using `any`!

That's right, generics:

```jsx
function identity<ArgType>(arg: ArgType): ArgType {
  return arg;
}
```

Once again, I find this syntax a bit complex to parse, but all it's really saying is: "the `identity` function can take any type (`ArgType`), and that type will be both the type of its argument and its return type".

And this is how you would use that function and specify its type:

```jsx
const greeting = identity<string>('Hello World!');
```

In this specific instance `<string>` isn't necessary since TypeScript can infer the type itself, but sometimes it can't (or does it wrongly) and you have to specify the type yourself.

### Multiple type variables

You're not limited to one type variable, you can use as many as you want. For example:

```jsx
function identities<ArgType1, ArgType2>(
  arg1: ArgType1,
  arg2: ArgType2
): [ArgType1, ArgType2] {
  return [arg1, arg2];
}
```

In this instance, `identities` takes 2 arguments and returns them in an array.

### Generics syntax for arrow functions in JSX

You might have noticed that I've only used the regular function syntax for now, not the arrow function syntax introduced in ES6.

```jsx
// an arrow function
const identity = (arg) => {
  return arg;
};
```

The reason is that TypeScript doesn't handle arrow functions quite as well as regular functions (when using JSX). You might think that you can do this:

```jsx
// this doesn't work
const identity<ArgType> = (arg: ArgType): ArgType => {
  return arg;
}

// this doesn't work either
const identity = <ArgType>(arg: ArgType): ArgType => {
  return arg;
}
```

But this doesn't work in TypeScript. Instead, you have to do one of the following:

```jsx
// use this
const identity = <ArgType,>(arg: ArgType): ArgType => {
  return arg;
}

// or this
const identity = <ArgType extends unknown>(arg: ArgType): ArgType => {
  return arg;
}
```

I would advise using the first option because it's cleaner, but the comma still looks a bit weird to me.

To be clear, this issue stems for the fact that we're using TypeScript with JSX (which is called TSX). In normal TypeScript, you wouldn't have to use this workaround.

### A word of warning on type variable names

For some reason, it's conventional in the TypeScript world to give one letter names to the type variable in generic types.

```jsx
// instead of this
function identity<ArgType>(arg: ArgType): ArgType {
  return arg;
}

// you would usually see this
function identity<T>(arg: T): T {
  return arg;
}
```

Using full words for the type variable name can indeed make the code quite verbose, but I still think that it's way easier to understand than when using the single-letter option.

I encourage you to use actual words in your generic names like you would do elsewhere in your code. But be aware that you will very often see the single-letter variant in the wild.

### Bonus: a generic type example from open source: `useState` itself!

To wrap up this section on generic types, I thought it could be fun to have a look at a generic type in the wild. And what better example than the React library itself?


Fair warning: this section is a bit more complex than the others in this article. Feel free to revisit it later if you don't get it at first.



Let's have a look at the type definition for our beloved hook `useState`:

```jsx
function useState<S>(
  initialState: S | (() => S)
): [S, Dispatch<SetStateAction<S>>];
```

You can't say I didn't warn you - type definitions with generics aren't very pretty. Or maybe that's just me!

Anyway, let's understand this type definition step by step:

- We begin by defining a function, `useState`, which takes a generic type called `S`.
- That function accepts one and only one argument: an `initialState`.
  - That initial state can either be a variable of type `S` (our generic type), _or_ a function whose return type is `S`.
- `useState` then returns an array with two elements:
  - The first is of type `S` (it's our state value).
  - The second is of the `Dispatch` type, to which the generic type `SetStateAction<S>` is applied. `SetStateAction<S>` itself is the `SetStateAction` type with the generic type `S` applied (it's our state setter).

This last part is a bit complicated, so let's look into it a bit further.

First up, let's look up `SetStateAction`:

```jsx
type SetStateAction<S> = S | ((prevState: S) => S);
```

All right so `SetStateAction` is also a generic type that can either be a variable of type `S`, or a function that has `S` as both its argument type and its return type.

This reminds me of what we provide to `setState`, right? You can either directly provide the new state value, or provide a function that builds the new state value off the old one.

Now what's `Dispatch`?

```jsx
type Dispatch<A> = (value: A) => void;
```

All right so this simply has an argument of type whatever the generic type is, and returns nothing.

Putting it all together:

```jsx
// this type:
type Dispatch<SetStateAction<S>>

// can be refactored into this type:
type (value: S | ((prevState: S) => S)) => void
```

So it's a function that accepts either a value `S` or a function `S => S`, and returns nothing.

That indeed matches our usage of `setState`.

And that's the whole type definition of `useState`! Now in reality the type is overloaded (meaning other type definitions might apply, depending on context), but this is the main one. The other definition just deals with the case where you give no argument to `useState`, so `initialState` is `undefined`.

Here it is for reference:

```jsx
function useState<S = undefined>(): [
  S | undefined,
  Dispatch<SetStateAction<S | undefined>>
];
```

## Using generics in React

Now that we've understood the general TypeScript concept of generic types, we can see how to apply it in React code.

### Generic types for React hooks like `useState`

Hooks are just normal JavaScript functions that React treats a bit differently. It follows that using a generic type with a hook is the same as using it with a normal JavaScript function:

```jsx
// normal JavaScript function
const greeting = identity<string>('Hello World');

// useState
const [greeting, setGreeting] = useState<string>('Hello World');
```

In the examples above you could omit the explicit generic type as TypeScript can infer it from the argument value. But sometimes TypeScript can't do that (or does it wrongly), and this is the syntax to use.

We'll see a live example of that in the next section.



If you want to learn how to type all hooks in React, stay tuned! An article on that subject will be out next week. Subscribe to be sure to see it!



### Generic types for Component props

Let's say you're building a `Select` component for a form. Something like this:

```jsx
import { useState, ChangeEvent } from 'react';

function Select({ options }) {
  const [value, setValue] = useState(options[0]?.value);

  function handleChange(event: ChangeEvent<HTMLSelectElement>) {
    setValue(event.target.value);
  }

  return (
    <select value={value} onChange={handleChange}>
      {options.map((option) => (
        <option key={option.value} value={option.value}>
          {option.label}
        </option>
      ))}
    </select>
  );
}

export default Select;

// `Select` usage
const mockOptions = [
  { value: 'banana', label: 'Banana 🍌' },
  { value: 'apple', label: 'Apple 🍎' },
  { value: 'coconut', label: 'Coconut 🥥' },
  { value: 'watermelon', label: 'Watermelon 🍉' },
];

function Form() {
  return <Select options={mockOptions} />;
}
```



If you're unsure about what's going on with the type of the `event` object in `handleChange`, I have an article explaining [how to use TypeScript with events in React](https://devtrium.com/posts/react-typescript-events)



Let's say that for the `value` of the options we can accept either a string or a number, but not both at the same time. How would you enforce that in the `Select` component?

The following doesn't work the way we want, do you know why?

```jsx
type Option = {
  value: number | string;
  label: string;
};

type SelectProps = {
  options: Option[];
};

function Select({ options }: SelectProps) {
  const [value, setValue] = useState(options[0]?.value);

  function handleChange(event: ChangeEvent<HTMLSelectElement>) {
    setValue(event.target.value);
  }

  return (
    <select value={value} onChange={handleChange}>
      {options.map((option) => (
        <option key={option.value} value={option.value}>
          {option.label}
        </option>
      ))}
    </select>
  );
}
```

The reason it doesn't work is that in one `options` array you could have an option with a value of type number, and another option with a value of type string. We don't want that, but TypeScript would accept it.

```jsx
// this would work with the previous `Select`
const mockOptions = [
  { value: 123, label: 'Banana 🍌' },
  { value: 'apple', label: 'Apple 🍎' },
  { value: 'coconut', label: 'Coconut 🥥' },
  { value: 'watermelon', label: 'Watermelon 🍉' },
];
```

The way to enforce the fact that we want _either_ a number _or_ an integer is by using generics:

```jsx
type OptionValue = number | string;

type Option<Type extends OptionValue> = {
  value: Type;
  label: string;
};

type SelectProps<Type extends OptionValue> = {
  options: Option<Type>[];
};

function Select<Type extends OptionValue>({ options }: SelectProps<Type>) {
  const [value, setValue] = useState<Type>(options[0]?.value);

  function handleChange(event: ChangeEvent<HTMLSelectElement>) {
    setValue(event.target.value);
  }

  return (
    <select value={value} onChange={handleChange}>
      {options.map((option) => (
        <option key={option.value} value={option.value}>
          {option.label}
        </option>
      ))}
    </select>
  );
}
```

Take a minute to understand the code above. If you're not familiar with generic types, it probably looks quite weird.

One thing you might be asking is why we had to define `OptionValue` and then put `extends OptionValue` in a bunch of places.

Well imagine we don't do that, and instead of `Type extends OptionValue` we just put `Type` instead. How would the `Select` component know that the type `Type` can either be a `number` or a `string` but nothing else?

It can't. That's why we have to say: "Hey, this `Type` thing can either be a string or a number".


It's a detail unrelated to generics, but if you use the above code in an actual editor you'll probably get a TypeScript error inside the `handleChange` function.

The reason for that is that `event.target.value` will be converted to a string, even if it was a number. And `useState` expects the type `Type`, which can be a number. So there's an issue there.

The best way I've found to handle this is by using the index of the selected element instead, like so:

```jsx
function handleChange(event: ChangeEvent<HTMLSelectElement>) {
  setValue(options[event.target.selectedIndex].value);
}
```

## Wrap up

I hope this article helped you to better understand how generic types work. When you get to know them, they aren't so scary anymore 😊

Yes, the syntax can get some getting used to, and isn't very pretty. But generics are an important part of your TypeScript toolbox to create great TypeScript React applications, so don't shun them just for that.

Have fun building apps!

PS: Are there other generic type applications in React that I should mention in this article? If so, feel free to ping me on [Twitter](https://twitter.com/PierreOuannes) or shoot me an email at pierre@devtrium.com.