Migrating a 50K SLOC Flow + React Native app to TypeScript

By Adam Terlson

In our case, the effort began with ~1800 TS compilation errors.

Fixing this amount of errors is not a linear journey where one fix means your errors go down to 1799. Sometimes, making a change will result in dozens fewer errors, making you feel like a god among mortals, and sometimes it goes way up causing you to wonder if you can do anything right.

#1—Fixing the Implicit Any

The number one thing TypeScript will complain about is an implicit any. Mostly due to differences between TS and Flow regarding type inference, one must immediately add additional annotations.

Be warned: don’t go simply adding a type explicitly where TypeScript complains. Instead, ask “Why is this thing any?”

// Problem hereconst reqs = get(props, 'requests', [])const inProgressIds = reqs.map(req => req.id)const activeRequests = actions // Error shown here

.filter(action => inProgressIds.includes(action.id))

Rather than adding a type to action as the error suggests, reqs is missing a type, producing the error below. The root cause could be another issue in the function, or even an entirely different file!

#2—Updating React components

Easily the least enjoyable and most “hairy” part of the conversion is migrating React code. Errors thrown in React code often have long error chains, sometimes with details that seem at the surface totally unrelated to the required fix.

Our project limits the use of React Native primitives from our “smart” components, choosing instead to create our own set of primitives in a reusable UI framework.

/* @flow */
import Button as RNButton from 'react-native'
// Seal to avoid warning about inexact spreadtype PropTypes = {| onPress: () => mixed, custom: boolean,

|}

// Trivialized component
const Button = (props: PropTypes) => <RNButton {...props } />

Under Flow, one must add every prop passed into <Button> to PropTypes, where instead we’d prefer to take all the props that <RNButton> does and add in our own custom one.

// Import props available for every React Native component! :D
import Button as RNButton, { ButtonProps } from 'react-native'
interface PropTypes extends ButtonProps { custom: boolean,

}

const Button = (props: PropTypes) => <RNButton {...props } />

Much preferable (and accurate)! Interfaces are one of the best parts about TypeScript!

#3—Refactoring HOCs

One benefit gained with TypeScript over Flow is the ability to use generic type arguments inside the body of your function. Not sure how advisable generally this is, but it’s a great feature to have when adding types for higher order components!

// TS allows the use of generics in the body of your HOC. Yes!
const withAThing = <T>(config: Config<T>) => (WrappedComponent: React.ComponentType<T>) =>

(props: T) => <WrappedComponent {...props } />

However, in order to leverage this feature, we more-or-less needed to completely rewrite our type definitions for all of our higher order components. Ouch.

Needless to say we‘d prefer hooks!

After: 96% TypeScript Coverage

$ type-coverage -p tsconfig.json # ignored tests + storybook68712 / 71471 96.13%$ time tsc
tsc 29.82s user 1.76s system 166% cpu 18.955 total

Coverage increase of ~12%. Execution time more or less identical.

By far the most significant type coverage gaps exist in our Redux Saga code. This was true under Flow as well, but it seems switching to TypeScript hasn’t improved matters much, if any.

Summary

If you’re still using Flow with React Native, it’s never been easier to switch!

It took the equivalent of 3 engineers 10 working days to accomplish the conversion, including time taken for regression testing, code review, refactoring, and developing new usage patterns.

The majority of errors can be easily solved with a google search for the error message, while a few required time, consideration, and experience to be confident in the fix.

Though we discovered a few bugs (mostly related to incorrect third party API usage), this was a minor benefit to the effort. Gaining some additional coverage on our React code did allow for some dead code elimination.

Overall, we achieved a codebase better poised for ongoing future development both in community support and developer experience.