Writing better Reducers with React and Typescript 3.4

Leveraging state reducers and immutability at scale can be tricky. Nonetheless the incredible arsenal of tools we have to handle that, it all comes around to a single pattern that resembles our application growth. Thus, application state is more than a challenge, is a narrative we tell ourselves every time we come back to the code.

When it comes to React, sometimes it doesn’t matter much how we approach designing the many pieces of state, but how we wire them together. We may rely on Redux, Context, useReducer, useState, or something totally different. But deep down all we want is that all pieces make sense when playing together. And for that, I was already convicted that Typescript can have an essential role, but now that version 3.4 is out, I’m thrilled.

Typescript is more than types.

People have a strong opinion about enforcing types in JavaScript. This makes me remember something meaningful I read once: we write code just like a writer writes proses.

JavaScript and Typescript discussions are generally about form and structure, and people usually forget about rhythm. After a few years using putting a .ts at the end of my files, it’s the latter I care the most, and on what Typescript can benefit us more.

In other words, instead of starting off writing interfaces for props, states and functions return’s, and making our feature too tied without writing a single line of runtime code, we spike into the code and use Typescript as we go, to connect the dots and make sure that future adjustments won’t compromise our app integrity. The bottom line is, we don’t have to do a TDD where T stands for Type.

Writing better Reducers

Before starting with code examples, we need to define better. So, a naive interpretation of what I want to achieve is:

  • Less error prone code: As my components grow, I want to focus on what is essential, and not having to manually check if the changes I’m writing are backwards compatible. Typescript automates that for me and even give me instant feedback.
  • No extra boilerplate for types definitions: The codebase I work on, indeed, has some generic interfaces that help me building up new types for special occasions. But I don’t think it’s productive (nor necessary) having to write helpers for every situation.
  • No code/type redundancy (DRY): We focus on letting our code to dictate our types and not the other way around.

Now, let’s build a reducer that encapsulates the state of a user. Assume we need to have a state like below:

const initialState = {
  name: '',
  points: 0,
  likesGames: true
}

type State = typeof initialState;

By doing type State = typeof initialState we already have 50% of the type definitions we’re going to write here. Also, the moment I make any change to that structure, it will be propagated to all the places I need it, highlighting what needs to be updated. Typescript will read State as:

type State = {
  name: string;
  points: number;
  likesGames: boolean;
}

For our actions, I’ll use the pattern where we define action creators beforehand, which I believe it’s the most common pattern out there. Also, for projects that are moving towards Typescript, this can be more friendly. An action can modify any amount of fields of the state at the same time. For simplicity, I’ll write an action creator for each field I have.

export function updateName(name: string) {
  return {
    type: ‘UPDATE_NAME’,
    name
  }
}

type Action = ReturnType<typeof updateName>

The ReturnType extracts the type of what is being returned from the function passed to its arguments. It’s available since Typescript 2.8. By using it, we don’t have to write twice the same object. But here is where the newest version (3.4) really shines. The problem with the above code is that, until now, Action would be interpreted as{ type: string, name: string } and thus, no useful information about the action itself would be considered when doing things like switch(action.type) or dispatch({ type: 'anything' }).

Const assertions

We now can lock down the type of any object and hint the compiler about immutability. From the release post,

  • Less error prone code: As my components grow, I want to focus on what is essential, and not having to manually check if the changes I’m writing are backwards compatible. Typescript automates that for me and even give me instant feedback.
  • No extra boilerplate for types definitions: The codebase I work on, indeed, has some generic interfaces that help me building up new types for special occasions. But I don’t think it’s productive (nor necessary) having to write helpers for every situation.
  • No code/type redundancy (DRY): We focus on letting our code to dictate our types and not the other way around.

Here’s our updated action using const assertions

export function updateName(name: string) {
  return <const>{
    type: ‘UPDATE_NAME’,
    name
  }
}

Resulting Type of the action

Finally, all typings for our actions is just as below, which now completes all the “extra” work we had in order to help another tool to give us better insights about our own code.

type Action = ReturnType<
 typeof updateName | typeof addPoints | typeof setLikesGames
>

Conclusion

Const assertions can be more helpful when writing reducers for Redux, as actions creators are more a common pattern for dispatching actions. Of course, this is just a tiny example of its capabilities. If you’re using Typescript with a similar approach than me and creating types as a consequence of the implementation, Reducers are just the beginning. A full gist of the example used here can be seen here.

#reactjs #typescript

Writing better Reducers with React and Typescript 3.4
51.10 GEEK