Static Types In Medias Res

Sid Raval

Several colleagues and I recently wrapped up a 5 month long React Native project. The client had come to thoughtbot with an idea, and went through our product design sprint process to come up with an MVP that we would start designing and developing.

One constraint the team faced was a hard 8 week deadline, due to an external event that could not be rescheduled. Additionally, part of the point of an MVP is to figure out if there is a strong market for your product. With these constraints in mind, the thoughtbot developers strategically chose to take on technical debt while building the MVP in the interest of getting something out the door.

One concrete form this technical debt took was starting the codebase without a static type checker for our JavaScript. Not all the team members were familiar with statically typed code, and so we chose to forego a typechecker so each developer could hit the ground running.

To everyone’s delight the initial launch of the product showed promise, and so our client chose to continue our relationship and work on the next version of the application. Given that we were now building an application with some longevity, the developers chose to look towards maintainability of the application before working on new features. We spoke with the client who agreed to two weeks of refactoring and bug squashing.

Based on the good experience two of the developers had in a past TypeScript + React Native codebase and the many well known benefits of statically typed code, the team decided to retrofit the codebase with a static type checker.

We chose to move forward with Flow because it proved much easier (than TypeScript) to introduce to a project mid-life. Sadly, TypeScript is a much more fully featured language that we would have preferred using.

Flow example

Benefits & Learnings

The holy grail of a well-typed Redux + React Native codebase is type checking all the way from the Redux store through your connected components. This ensures that changes to your data modeling (e.g. changes to reducers or changes to the store’s shape) are propagated to your components. With this goal in mind, we started our retroactive typing process by by adding types to our Redux modules, one by one. This proved to be a good choice for a few reasons:

  • Modeling the data at the state/reducer level laid the groundwork for typing our React components.
  • The types at the Redux action/reducer/state level are quite simple, generally product types that can be modeled via Flow’s object types. For developers on our team that were new to static typing, starting with simple types was more manageable than, for instance, the type of the connect function in react-redux.

In addition to all the usual benefits one gets from type safety, the team felt a few that were particular to adding types as part of a ‘refactoring’ workflow:

  • We started by modifying the application code as little as possible, i.e. adding types that reflected the current codebase, rather than both modifying the code and adding types simultaneously. This made reviewing the pull requests much less scary, and allowed us to notice and document ‘weird’ types as something to revisit.
  • As we added types to the components, we uncovered possible states & code paths we were not taking care of. The team knew these existed, since we consciously ignored some states because they were unlikely or affected a small amount of the userbase. Adding the types revealed edge cases and possible sources of bugs that we were able to add to our backlog or squash as they popped up.
  • Adding types revealed sub-par data modeling. There were too many places we were passing JavaScript objects around with no idea of what properties to expect on the objects.
  • Adding types to code we’d written automatically gave us time to review the codebase and reevaluate some architectural decisions we’d made. This allowed us time to document pain points and open up discussion on how to improve the code.

Pain Points & Downsides

We encountered several pain points throughout the process, though we felt they were outweighed by the upsides:

  • Third party type definitions. We used flow-typed for type annotations for some of our dependencies. Since these annotations are decoupled from the actual implementation of the libraries, there are occasional warts and inconsistencies that we had to deal with. For example, the team had to be diligent about ensuring the versions matched between our dependencies and the type definitions of those dependencies.
  • Increased toolchain complexity. We had to configure our continuous integration environment to type check as part of its steps. We had to ensure everyone’s editors were configured to deal with flow type annotations properly. The team consisted of emacs/vim users, but most of us temporarily switched to Visual Studio Code for its seamless flow integration.

Final Thoughts

The team considered this experiment a success, and put in a similar scenario we would again advocate for types-after-the-fact as an effective tool for refactoring, establishing long term maintainability, bug catching, and ensuring correctness of application code.