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.
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.
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.
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).
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:
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!