The bullet proof useReducer - Typescript (2/2)
Tags: typescript, react, webdev, javascript
Published: 2022-04-06 21:21:52

_This blog post takes by granted that you are aware of `useReducer` logic and the basics regarding TypeScript._ 

## Bringing TypeScript to the table

In the previous [blog post](https://dev.to/pffigueiredo/mastering-usereducer-12-5d2e) we went in full detail on how to leverage React's `useReducer`, but there is still something missing for it to be fully **bullet proof** - TypeScript.

### Why does it help

When applying TypeScript into `useReducer` you will not only feel a sense of **security**, but also, feel a lot more confident to touch code and modify any logic related with the states inside the `reducer`.

**Pros** of using TypeScript: 
- Type safety
- Type completion
- Makes sure all states are handled
- Makes sure an Action sends the correct data

**Cons** of using TypeScript
- Brings a little more complexity
- Makes it harder to hack in a new state quickly

From where I see it, the **pros** overcome the cons by a lot and as such, I strongly advise you to add some sort of typing to your code.

## Typing `fetchReducer`

In the last post, we finished with this plain JS reducer:

![JS reducer print](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cplf7fg4rjsrth1xbxri.png)

### Typing Actions

To start, we need to type the different possible actions, so that we have type completion depending on what we are dispatching.

1- **Define Action**
```ts
type Action<DataT, ErrorT> ={}
```

Action object needs to take in **two generics**, one for the Data type and one of the Error type.

2- **Define FETCH**

```ts
{ type: "FETCH" }
```

For `FETCH` we really only need to define the `type`'s property type, which is a string literal and nothing more.

3- **Define RESOLVE**

```ts
{ type: "RESOLVE", data: DataT }
```

When we dispatch `RESOLVE` it means that the fetch was successful and we already have the data - this action ensures that when we do `dispatch({type: "RESOLVE"})` there is a type error for not passing the data.

4- **Define REJECT**

```ts
{ type: "REJECT", error: ErrorT }
```

`REJECT` acts pretty much as the success action, meaning, that when we dispatch this action, TypeScript will make us pass an error along.

5- **Union of actions**
```ts
type Action<DataT, ErrorT> =
  | { type: "FETCH" }
  | { type: "RESOLVE"; data: DataT }
  | { type: "REJECT"; error: ErrorT };
```

Our action final type is just an `union` of all our defined actions, meaning, it can take any of those forms.

### Typing States

In order to add more strictness to our reducer, each one of the `states` should have their own type definition. 

All of these states must have the same properties, `status`, `data` and `error`, but for each one of the states, these properties will have their own type definitions, depending on the situation.
 
1- **Typing `iddle`**
```ts 
type IddleState<DataT> = {
  status: "idle";
  data: Nullable<DataT>;
  error: null;
};
``` 
The `iddle` state takes the `DataT` generic, so that it allows the reducer to start with initial data. Everything else is pretty standard for all the other reducer states.


2- **Typing `loading`**
```ts 
type LoadingState<DataT, ErrorT> = {
  status: "loading";
  data: Nullable<DataT>;
  error: Nullable<ErrorT>;
};
``` 
The `loading` state needs to take both `DataT` and `ErrorT` generics, as it depends too much on the implementation details if we wanna show or not errors while fetching new data.

3- **Typing `success`**
```ts 
type SucessState<DataT> = {
  status: "success";
  data: DataT;
  error: null;
};
``` 
The `success` state only needs the `DataT` generic and we can already define the `error` property can be nothing but `null`, this way, we protect our selves to set errors while in the `success` state (impossible state)!

4- **Typing `failure`**
```ts 
type FailureState<ErrorT> = {
  status: "failure";
  data: null;
  error: ErrorT;
};
``` 
The `failure` state behaves pretty much like the `success` one, but in the opposite direction, by setting the `error` needs a value and that the `data` must be of the `null` type.

5- **Union of States**
```ts
type State<DataT, ErrorT> =
  | IddleState<DataT>
  | LoadingState<DataT, ErrorT>
  | SucessState<DataT>
  | FailureState<ErrorT>;
```

Just like our `Action` type, `State` is also just an union of all the possible states that our reducer can return

### Typing reducer function

Now that we have all our states and actions properly typed, it's just a matter of adding those to `fetchReducer` function it self.

1- **Adding generics to the function**  
```ts  
function fetchReducer<DataT, ErrorT = string>(
    currentState,
    action
  ){
...
}
```
We defined `ErrorT` as an optional generic by defining it as `string` by default.

2-**Typing the arguments and the return type**
```ts  
function fetchReducer<DataT, ErrorT = string>(
    currentState: State<DataT, ErrorT>,
    action: Action<DataT, ErrorT>
  ): State<DataT, ErrorT> {
...
}
```

We just need to take our existing `Action` and `State` defined types, and add them to the **respective parameters**.

For the **return type**, it was also just a matter of defining that this reducer, can only return any of the states that is inside the `State` union type.

### Typing `useFetchReducer`

Although the `reducer` function is already properly typed, we still need to add typing to our custom `useReducer` hook.

1- **Passing the generics to the `useFetchReducer`**

```ts
// added the generics here
function useFetchReducer<DataT, ErrorT = string>(
  initialData
){

// removed them from the reducer
  function fetchReducer(
    state: State<DataT, ErrorT>,
    event: Event<DataT, ErrorT>
  )
}
``` 

By providing generics to the `useFetchReducer` hook, we don't need to have them on the reducer's signature anymore, as we can use the ones provided above and keep things consistent.

2-**Typing `initialData` argument**

```ts
function useFetchReducer<DataT, ErrorT = string>(
  initialData: Nullable<DataT> = null
): [State<DataT, ErrorT>, React.Dispatch<Action<DataT, ErrorT>>] {...}
```

As far as `initalData` goes, if you wanted to pass in anything, it would have to be the same type that you defined your generic previously.

3-**Typing `initialState` constant**

```ts
  const initialState: IddleState<DataT> = {
    status: "idle",
    data: initialData,
    error: null,
  };
```

We should use the `IddleState` type for the `initialState` constant, this way, if we decide to change it, TypeScript will make sure they are in sync.

### The final type

```ts
import { useReducer } from "react";

type Nullable<T> = T | null | undefined;

type IddleState<DataT> = {
  status: "idle";
  data: Nullable<DataT>;
  error: null;
};

type LoadingState<DataT, ErrorT> = {
  status: "loading";
  data: Nullable<DataT>;
  error: Nullable<ErrorT>;
};

type SucessState<DataT> = {
  status: "success";
  data: DataT;
  error: null;
};

type FailureState<ErrorT> = {
  status: "failure";
  data: null;
  error: ErrorT;
};

type State<DataT, ErrorT> =
  | IddleState<DataT>
  | LoadingState<DataT, ErrorT>
  | SucessState<DataT>
  | FailureState<ErrorT>;

type Event<DataT, ErrorT> =
  | { type: "FETCH" }
  | { type: "RESOLVE"; data: DataT }
  | { type: "REJECT"; error: ErrorT };

function useFetchReducer<DataT, ErrorT = string>(
  initialData: Nullable<DataT> = null
) {
  const initialState: IddleState<DataT> = {
    status: "idle",
    data: initialData,
    error: null,
  };

  function fetchReducer(
    state: State<DataT, ErrorT>,
    event: Event<DataT, ErrorT>
  ): State<DataT, ErrorT> {
    switch (event.type) {
      case "FETCH":
        return {
          ...state,
          status: "loading",
        };
      case "RESOLVE":
        return {
          status: "success",
          data: event.data,
          error: null
        };
      case "REJECT":
        return {
          status: "failure",
          data: null,
          error: event.error,
        };
      default:
        return state;
    }
  }

  return useReducer(fetchReducer, initialState);
}
```

After all this typing, we should be pretty safe when trying to access any reducer's state or even when dispatching actions.

**Dispatching Actions**

![dispatching actions gif](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/trp9rq8xkk4kl5mg0d9y.gif)

As you can perceive from this GIF, TypeScript doesn't allow us to pass in incorrect Actions into the dispatcher function 

**Accessing reducer's state**
![accessing state gif](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k3neld7x4kik8yzp6jqi.gif)

If you look closely, you will notice that TypeScript can infer what's the data and error types by the current state.

This feature it's called [Discriminating Unions](https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html) and it works by having a **Discriminator** property in each one of the union types, that can help TypeScript narrow down which is the current state - in our case it is the `status`, which is unique for each of union types.

## Conclusion

By using TypeScript in conjunction with the `useReducer` hook, you will be able to create **robust React UI's**, as well as iterate on top of them with much more confidence.

Summarizing everything we discussed above, these are the steps you should take to create a properly typed `useReducer` hook:

1- **Type each `action` individually** and create a super type, which is the union of all of them;
2 - **Type each `state` individually** and create a super type, which is the union of all of them;
3 - Add the necessary generic types to the `useReducer` and `reducer` function.

And that's it, you just improved your Developer Experience by a lot, and not only that, but by doing all these typing, you ended up creating a **thin testing layer** that will probably spare you from many coming bugs.
 
---

_Make sure to follow me on [twitter](https://twitter.com/P_FFigueiredo) if you want read about TypeScript best practices or just web development in general!_