Photo by on Sergey Pesterev Unsplash is the prototypical response when asking a software engineer for advice, no matter how straight-forward the problem may seem. When asked “Should we choose TypeScript or Flow for our next React Native project?” however, the answer only depends on one variable: . “Well, it depends” whether or not you work at Facebook It’s interesting to consider how we got here. The project team evaluated Flow vs TypeScript for our new React Native app almost three years ago. At the time, TypeScript didn’t support React well, didn’t allow for a gradual opt-in, there was no Babel support, and VSCode wasn’t the editor providing the best JavaScript development experience on the market. None of these factors are true today. But why bother to migrate? TypeScript has a lot of value to offer making the switch highly attractive: More “future proof” Higher OSS adoption & better RN + library support Awesome developer experience (VSCode) Consistency with other department projects Easier to hire for So, we forged ahead! But how’d it go? Before: 50K SLOC, 84% Flow Coverage $ npx sloc app ---------- Result ------------ Physical : 57903 Source : 50859 Number of files : 709 ---------------------------- $ npx flow-coverage-report -i percent: 84 % $ time flow check flow 3.64s user 2.31s system 32% cpu 18.458 total read 'app/**/*.js' Getting Started The first step we took was to update our JS tooling configuration. For our project, the procedure was relatively straight-forward, guided mostly by simply copying the configurations directly from a newly instantiated TypeScript template project using the . react-native cli Prettier & ESLint { : } "parser" "typescript" One used to have to deal with TSLint when chosing TypeScript but thankfully , so it’s an easy choice in 2019—just stick with ESLint! TSLint is deprecated Literally no changes necessary (for our usage anyway, and that included plugins and custom rules!) Done. Next. Bonus Aside: Removing Black Magic module imports Did you know that you can import JavaScript modules relative to a package.json file using the name attribute in the json file, no plugins required? Black magic! But it was useful black magic! To accomplish absolute path imports (i.e. we included in our project a in the (and several others as well for a total of 4 package.json’s): import Foo from ‘app/ui/components/Foo') package.json /app directory { : } "name" "app" This even works when importing from other folders in the tree (meaning you can import from outside of the ‘app’ folder). app/components/Foo Well, TypeScript doesn’t like this black magic fuckery and couldn’t figure out our imports :(. So, we deleted our extra package.json files and chose to use babel instead. Babel With built in Babel support for TypeScript in Babel 7, converting our babel configuration from Flow to TypeScript was a process of simply removing unused plugins (like ). @babel/plugin-transform-flow-strip-types Here’s our final Babel.config.js, complete with less magical module resolution. .exports = { : [ ], : [ [ , { : [ ], : { : }, }, ], ] } module presets 'module:metro-react-native-babel-preset' plugins 'module-resolver' root '.' alias app './app' tsconfig.json While not strictly required for running your React Native app (Babel will just strip/ignore the TS syntax anyway), the tsconfig is required for using the TypeScript compiler (and VSCode tooling) to detect type errors. Thankfully, we got 90% of the way there by simply copy-pasting the tsconfig from a boilerplate new React Native project. However, we also include some files in our project, which require some additional configuration. .json { : { : , : , : , : , : , : , : [ , ], : , : , : , : , : , : }, : [ , , , , ] } "compilerOptions" "baseUrl" "." "allowJs" true "allowSyntheticDefaultImports" true "esModuleInterop" true "isolatedModules" true "jsx" "react-native" "lib" "es6" "dom" "moduleResolution" "node" "noEmit" true "strict" true "target" "esnext" "resolveJsonModule" true // Required for JSON files "skipLibCheck" true "exclude" "node_modules" "e2e" "**/*.json" // Don't try and check JSON files "**/*.spec.ts" Migrating Flow to TS syntax and .js to .ts/x Thankfully some work has been done in the open source community to aid in the conversion from Flow to TypeScript syntax, and the projects have support for many of the common language features. However, I cannot say that these projects tend to be very well maintained or mainstream. There are two dominant solutions available, and . We tried the flow-to-typescript library first but abandoned it because it crashed on any file containing a function ( ). flow-to-typescript babel-plugin-flow-to-typescript WTF? Thankfully the babel-plugin-flow-to-typescript worked for us. Almost. As of writing the same related to function support forced us to use . I don’t know what’s up with these tools and supporting plain old functions but whatever, it worked, I guess I should be thankful! bug a fork Convert JavaScript + Flow with a .js extension to TypeScript with .ts: yarn add global @babel/cli @babel/core babel-plugin-flow-to-typescript npx babel script.js -o script.ts --plugins=babel-plugin-flow-to-typescript find app - f -name | parallel # Convert single file # Convert all files and delete original JS after conversion # Prereq: 'brew install parallel' type '*.js' "npx babel {} -o {.}.ts && rm {}" Note: We actually used @zxbodya/babel-plugin-flow-to-typescript due to a bug in the main repo. Next, rename any files which import React at the top to use .tsx instead: find app/components -name - sh -c {} \; "*.ts" exec 'git mv "$0" "${0%.ts}.tsx"' Sanity Check: Does the app still run? Before proceeding any further, it’s worth checking at this point that the project actually runs. Because Babel is used to compile TypeScript to JavaScript, and Babel simply strips out all TypeScript related syntax, regardless of how many type errors your project has it should “Just Work.” In our case, it took less than half a day to update configurations, rename files, migrate all syntax, and get a running app again. But the work is obviously still far from over. Fixing 1000s of compilation errors 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 increases your overall error count leaving your confidence shaken! #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: . Instead, ask don’t go simply adding a type explicitly where TypeScript complains “Why is this thing any?” reqs = get(props, , []) inProgressIds = reqs.map( req.id) activeRequests = actions .filter( inProgressIds.includes(action.id)) // Problem here const 'requests' const => req const // Error shown here => action Rather than adding a type to as the error suggests, is missing a type, producing the error below. The root cause could be another issue in the function, or even an entirely different file! action reqs #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. Button RNButton type PropTypes = {| onPress: mixed, : boolean, |} Button = <RNButton {...props } /> /* @flow */ import as from 'react-native' // Seal to avoid warning about inexact spread => () custom // Trivialized component const ( ) => props: PropTypes Under Flow, one must add every prop passed into to , where instead we’d prefer to take all the props that does and add in our own custom one. <Button> PropTypes <RNButton> Button RNButton, { ButtonProps } interface PropTypes extends ButtonProps { : boolean, } Button = <RNButton {...props } /> // Import props available for every React Native component! :D import as from 'react-native' custom const ( ) => props: PropTypes 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! withAThing = <T>) => (WrappedComponent: React.ComponentType<T>) => (props: T) => <WrappedComponent {...props } /> // TS allows the use of generics in the body of your HOC. Yes! const (config: Config < > T 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 $ -coverage -p tsconfig.json 68712 / 71471 96.13% $ time tsc tsc 29.82s user 1.76s system 166% cpu 18.955 total type # ignored tests + storybook 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 working 10 full 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.