Refactoring A Live App To TypeScript


Andrew Vo-Nguyen

November 3, 2021

So recently, one of my clients had asked for some new features for a Dog Walking mobile app that was deployed earlier this year. I hadn't touched the code base in months and when I re-opened the code base to see how I would implement these features, it dawned on me that it was a bit of a dumpster fire 🔥. Keeping in mind that this was written about a year ago, a time where my programming skills weren't anywhere as polished as it is today. I decided, that the only way moving forward (with confidence) was to upgrade both the backend (Node.js/JavaScript) and frontend (React Native/JavaScript) to TypeScript.

I recently adopted TypeScript in other projects and had not look back since. The mission at hand here was to upgrade the code base without disrupting the daily users of the app. The first task was to upgrade the backend cloud functions and then the front end mobile app itself.

Backend 💾

The backend took a total of 2 weeks to refactor. A day or two was spent trying to get TypeScript to compile and play nice with Google Cloud Functions, which of course is read in JavaScript. Another week was spent testing each individual cloud function (49 in total). While refactoring to TypeScript it was a good opportunity to also upgrade packages and make trim the fat off redundant code. The biggest mistake I made while refactoring was adding and removing existing code to improve the overall project. This made it so it was not a 1 to 1 refactor, but it can't be helped to repair something when you come across something that is broken, or you feel can be improved.

The saving grace of this tedious refactor was configuring the IDE with strict ESLint rules and to use prettier to format the code. Another house keeping extension which kept the code well organised was sort-imports. When doing something so repetitive, it is easy to lose concentration and good developer tools help check your sanity. Here is a copy of the ESLint configuration:

2  "extends": [
3    "eslint:recommended",
4    "plugin:@typescript-eslint/recommended",
5    "prettier"
6  ],
7  "parser": "@typescript-eslint/parser",
8  "parserOptions": {
9    "project": [
10      "tsconfig.json"
11    ],
12    "sourceType": "module"
13  },
14  "ignorePatterns": [
15    "/lib/**/*", // Ignore built files.
16    "webpack.config.js"
17  ],
18  "plugins": [
19    "@typescript-eslint"
20  ],
21  "rules": {
22    "@typescript-eslint/ban-ts-comment": 0,
23    "@typescript-eslint/explicit-function-return-type": 2,
24    "@typescript-eslint/explicit-module-boundary-types": 0,
25    "@typescript-eslint/no-explicit-any": 0,
26    "@typescript-eslint/no-unused-vars": [
27      2,
28      {
29        "argsIgnorePattern": "^_"
30      }
31    ],
32    "no-duplicate-imports": "error",
33    "no-unused-vars": 0,
34    "object-shorthand": "error",
35    "sort-imports": [
36      "error",
37      {
38        "allowSeparatedGroups": false,
39        "ignoreCase": false,
40        "ignoreDeclarationSort": false,
41        "ignoreMemberSort": false,
42        "memberSyntaxSortOrder": [
43          "none",
44          "all",
45          "multiple",
46          "single"
47        ]
48      }
49    ],
50    "spaced-comment": "error"
51  }

Although refactoring to TypeScript would ultimately eliminate bugs, ironically it did introduce some bugs which unfortunately, due the the app being live, caused some disruptions to the app's customers. The bugs did not happen because of TypeScript, but due to logical mistakes while refactoring, which cannot be caught by TypeScript and ESLint. For example, I was missing a simple if statement inside a loop which caused 30+ notifications to be sent to many of the app's users at once 😰. I was highly apologetic for that. The beauty of cloud functions is that any bugs could be narrowed down to a single cloud function and could easily be re-deployed immediately.

Frontend 📱

I predicted that the frontend would also take roughly 2 weeks to refactor. I had never written a React Native app in Typescript and did not know what to expect. Fortunately, I could copy over the class models from the backend and re-use them on the frontend. The sheer amount of components and screens on the frontend made this refactor a more arduous task than the backend. In the end it took 3 weeks of coding, with beta tests ongoing right now.

For the workflow configuration there was a bit more work to get TypeScript to play nice with React Native. There had to be specific configurations/plugins for ESLint for React Native related features and Babel had to be configured to support the absolute path imports configured in TypeScript. Once again, the TypeScript refactor was a good opportunity to upgrade existing packages and to improve on existing code. This would give the final result more value for the customer.

Unlike the backend, testing and redeploying is not as simple as redeploying functions on demand. The entire app must be thoroughly tested before submitting the Apple App Store and Google Play Store (this process can take up to 24 hours to be approved). Because this is live app, we don't want the existing users to be the guinea pigs of this refactor, considering they had a perfectly working app prior to the refactor. This is where TestFlight comes to the rescue. TestFlight allowed me to give a select number of users a copy of the new app and for them to report any bugs using TestFlight. If the app was unusable, they had the option to revert back to the stable version in the app store. This completely mitigated the risk of disrupting existing customers.

Final Thoughts 👊

  • Refactoring was highly repetitive, and it felt like I was re-writing the app from scratch. So having patience and reminding myself that the reason I am doing it is for the long term goals of the project helped a lot. Whilst refactoring, and even while making the decision to refactor, the question always begged, "is this pursuit going to be worth it?", however once it is done, the answer is always "yes".
  • Logical errors cannot be caught by TypeScript, so I had to prepare to deal with them when they occurred. In the end, we are just humans and humans make mistakes.
  • I used logging tools on the backend sparingly to monitor the cloud functions to ensure they were doing what they were supposed to be doing.
  • I implemented a back up plan for when something went wrong, i.e. a kill switch in case an infinite loop happens, or an easy solution for rolling back code.
  • In the future, I would test more often incrementally to catch bugs as I refactored, rather than code first, test later. Test driven development would have helped immensely but would have added a significant time overhead.
  • Once it was all over, I was super proud of what I had done to the code bases and more confident in implementing new features in the future, instead of being nervous about whether or not the existing JavaScript would break, not as a matter of if, but when.