Tagged Unions, React, and Redux

Written by kaw2k | Published 2016/12/28
Tech Story Tags: react | javascript | typescript | flow | redux

TLDRvia the TL;DR App

Typescript and Flow are pretty cool for a bunch of reasons. I wanted to highlight one such case while using them with React. Specifically, we will see how tagged unions can give us better documentation and assurances with redux connected components. This article will assume some basic understanding of types.

The need

Let’s start by building something, a simple chat app. It will fetch the logged in user, the list of users chatting, and render it all to the screen. Nothing terribly exciting, but the app has some interesting states it can be in. For example it can be: offline where we won’t see other users, loading where we will see some loading indicator, and a loaded state where we will see ourself along with the other users.

Connecting our component

To get the ball rolling, here is a quick and dirty solution to hooking up our component to our redux store. We won’t bother writing the view or the connect function yet, we will focus on the return value of the mapStateToProps function which gets passed down as props to our view.

type MapStateToPropsResult= { loading?: boolean, disconnected?: boolean, users?: User[], activeUser?: User}

Simple enough: an object with all optional properties. This is totally usable; we could continue on our merry way and the type system would happily make us check if each property exists before we use them.

Do you see any problems though? The business requirements have clear relationships between these properties, but none of it can be inferred by looking at this code. It doesn’t make sense that we would have users or activeUser while the screen is loading. Also, it wouldn't make sense if we could talk with users while the app knows it is offline. All of these subtleties are lost if we simply make everything optional.

Secondly, we missed a valuable opportunity to use the type system as a tool. It is all too easy to simply go about types passively, write code and the compiler is just there as a safety net to make sure you don’t make any typos. If we actively think about how types work in our system, we can have the compiler guide us to correct solutions.

With this in mind, let’s revise our type signature.

type MapStateToPropsResult= { uiState: 'loading' }| { uiState: 'disconnected', activeUser: User }| { uiState: 'loaded', activeUser: User, users: User[] }

We have taken our single object of all optional properties and split it into a union of three objects with all required properties. Now, when you look at this type, you can quickly see the relationship between properties and what state the UI is in. It is now clear that when the app is loading we won’t have access to user or activeUser. Similarly, we wont have access to users when we are disconnected.

Lastly, we can leverage this type to make the compiler guide us to writing code that adheres to our business logic!

@connect((state, props): MapStateToPropsResult => {

})class UserList extends React.Component// ...

We have stubbed out our class and mapStateToProps function and told the compiler that this function will return the type we just defined. Right away the compiler sees that we are not returning a value of the union type and it will start to guide us to correct code.

@connect((state, props): MapStateToPropsResult => {const activeUser = selectActiveUser(state) // User | undefinedconst users = selectOtherUsers(state) // User[] | undefined

if (activeUser) {  
    return { uiState: 'loaded', activeUser, users } // ERROR  
}  

})class UserList extends React.Component// ...

Above for example, perhaps we have finished fetching the activeUser data, but we are still waiting on users to come back from the server. Since both users and activeUser are required in loaded, this type will force us to render a loading state instead.

By making all the properties in the type required, we are telling the compiler to force us to deal with loading states.

The view

Now how is this beneficial from the view’s perspective? For starters, now we won’t be able to access properties within the union until we figure out what state we are in.

class UserList extends React.Component {render() {const user = this.props.activeUser // Error!}}

For example above, the type system doesn’t know if we are loading etc, therefore it can't be assured that activeUser exists on our props. So what would a real example look like?

class UserList extends React.Component {renderLoading() { /* ... */ }renderDisconnected(activeUser) { /* ... */ }renderLoaded(activeUser, users) { /* ... */ }

render() {  
    const { uiState } = this.props  
    if (uiState === 'loading')  
        return this.renderLoading()

    const { activeUser } = this.props  
    if (uiState === 'disconnected')  
        return this.renderDisconnected(activeUser)

    const { users } = this.props  
    if (uiState === 'loaded')  
        return this.renderLoaded(activeUser, users)  
}  

}

This is just one way we could approach the view logic. It is interesting to see how more props become available to us as we narrow down what the union can and can’t be. After the first conditional where we check if uiState === 'loading' the type system can then infer that the remaining states both contain activeUser and therefore lets us access it via props.

Another interesting aspect is it can inform us if we are missing cases in our render function. Perhaps down the road we add another case to our union, away, and it is rendered with slight differences. The type system will then pick up on this change and let us know that our render function doesn't always return JSX, (assuming we type the return value of the render function).

Last thoughts

If you have stuck with me thus far, you may be able to tell that I am quite jazzed about this. Dynamic languages are wonderful for many, many reasons. However coming to the world of typed languages, there are entirely new realms of expressive power just waiting to be exploited.

I know there is some adage “Work will always fill all available time.” Well, in my case it is “If there is a lazier way to do it, I still wouldn’t do it because it probably would take too much effort.” Any opportunity I can get to have some one else force me to do my job properly is a huge win. In this case, I can (intentionally) have a cold, heartless, omnipotent computer force me to actually do my job properly.

With all this said and done, here are a few bullet points for posterity sake:

  • Union types are helpful outside of raw data. Use them for return values coming out of connect or props into your components. Really, they are really, really awesome.
  • If you see multiple optionals start creeping into your types, consider breaking it into a union where things are not optional.
  • Think through your types first. If you tell the compiler the shape of the function and it’s results up front, it will help you write correct code.
  • I have really enjoyed reading this piece by Yaron Minsky, this one by Scott Wlaschin, and this one by Richard Feldman. They all deal with structuring types to make states that shouldn’t happen, not happen. They are all super illuminating and are all applicable across languages.
  • Lastly, types give us a unique opportunity to embed and enforce business logic at compile time. It truly is worth it to add a few characters for the safety and documentation they will provide down the road.

Hacker Noon is how hackers start their afternoons. We’re a part of the @AMIfamily. We are now accepting submissions and happy to discuss advertising &sponsorship opportunities.

To learn more, read our about page, like/message us on Facebook, or simply, tweet/DM @HackerNoon.

If you enjoyed this story, we recommend reading our latest tech stories and trending tech stories. Until next time, don’t take the realities of the world for granted!


Published by HackerNoon on 2016/12/28