Found’s frontend application started as a standard javascript based React app, but as our product and company grew so did the complexity of our frontend application. As a result, in 2023 we reached a point where continuing to go without types in our frontend was causing our velocity to slow and bugs to increase. This post focuses on our journey of migrating our frontend codebase from javascript to typescript and how we used Grit, a modern AI tool, to help us.
A javascript React app is a great choice when starting a new project as you work towards a proof of concept or MVP of an initial product. At Found our frontend application started much the same way. Javascript is one of the most commonly used programming languages which allows for greater contribution from an engineering team and at Found this enabled us to move quickly and build the core frontend systems that our customers rely on today. However, as our engineering team grew, and the size of our frontend codebase increased to thousands of files several problematic issues started to arise:
Managing state across the app became more complex with more validations needed to make sure data was present or in the correct format.
API responses not being well defined which led to developers assuming optional data was always present.
Refactoring code leading to hidden bugs being introduced in contracts between components.
Onboarding new team members to our frontend codebase took more time due to implicit data contracts and undocumented data structures.
With a clear understanding of the problems we were facing we evaluated migrating to typescript and the benefits it offered. In doing so it became clear that moving to typescript would eliminate several of our problems and reduce others, but the process by which we would migrate to it was not as clear.
Our migration process began by focusing on generating a plan of which files to migrate first, then exploring tooling to help facilitate the process, and lastly deciding on a wholesale or incremental approach.
Our frontend codebase consisted of thousands of files and we wanted to focus on first migrating files that would have the most impact to our engineering team. We did this by first splitting files into two segments, the data layer and the presentation layer. We then further broke these down into smaller segments based on dependency hierarchy and what the file was used for.
In this segment we looked at what types would provide a good foundation to build off of and which were most critical for the continued evolution of our app. Our first choice was our API wrappers which we could then use to build our state management types off of, and then lastly build our hook and utility types off of those.
Our file structure fit this pattern well and allowed us to target specific folders in our migration first. We chose early on to group files by their type and then create sub-groups based on where they are used.
src/
|--contexts/
|--hooks/
| |--auth/
| |--payments/
| ...
|--redux/
| |--actions/
| |--reducers/
| |--selectors/
|--utilities/
| |--api/
| |--validations/
| ...
In this segment when looking at our component tree we wanted to focus on leaf nodes first and then work our way up the tree. This would allow our presentation layer types to start simple, and similarly to the data layer, we could easily build off them as we went along.
This also mostly aligned with our component file structure, and also highlighted the importance of consistent grouping of interconnected components. Our component files aren’t as organized and we didn’t group all component files together based on their usage. Had we followed a similar pattern to our business logic files we could have helped speed up our work.
src/
|--components/
| |--form/
| | |--dobInput.tsx
| | |--passwordInput.tsx
| | ...
| |--modal/
| | |--header.tsx
| | |--body.tsx
| | ...
|--pages/
| |--onboarding/
| | |--personalInfo.tsx
| | ...
| |--login.tsx
| |--register.tsx
With our strategy solidified we then moved on to the “how” of the migration process. At Found we prioritize engineering efficiency and maintaining a consistent velocity of shipping features. This made the idea of manually migrating every file a non-starter for us as it would consume a significant amount of development time and team members to accomplish in a reasonable timeline. Our requirements for evaluating tools was:
Does not sacrifice type safety for migration speed
We would prefer few, if any, // @ts-ignore and “any” types in migrated files
Allows us to make both small and large batches of file migrations
Some sections of our codebase require more careful review of the types generated than others.
Can easily update types generated
Our opinions change as we get more familiar with a language or type system so we want a tool that can adapt to those changes.
Does not require a dedicated engineer / team to ensure migration success
Today there are many AI tools that use your repo as context and integrate directly with your IDE. At the time we started this process in 2023 there were limited options for tool assisted typescript migrations and below are the ones we evaluated using.
Airbnb’s ts-migrate tool is one of the most commonly used methods for migrating to typescript and many a blog post has been written about the process of using it. This was initially our first choice to use when evaluating tools, but in practice it didn’t meet all our requirements. When testing it out over multiple sections of our codebase we were happy with how quickly it was able to transform files, but due to the complexity of some areas of our codebase we were left with too many “any”, “unknown”, “object” types and // @ts-ignore comments.
Ts-migrate got the job done quickly, but we would still need to dedicate a lot of time implementing types correctly and fixing commented out errors. This felt like a brute force approach that would complete the migration, but wouldn’t leave our code in that much of a better state.
Several companies with large codebases have successfully used codemod scripts to convert their codebase to typescript. Stripe is a good example of one such company. This method was successful for them in large part because of Stripe’s extensive use of Flow and the codemod process utilized this by transforming the Flow types into the equivalent Typescript types. We could have written our own codemod, but in a codebase like Found’s that had no type coverage at all (no Flow usage, and few proptypes defined) this would have been equivalent to using Ts-migrate which we already established as not being our best option.
Grit is a tool that uses a custom query language called GritQL and LLMs to analyze a codebase and make changes to it. Grit is designed to continuously run autonomously following a predetermined migration plan. It can be used to facilitate many types of code migrations with typescript being one of the most common.
This tool looked promising, but we wanted to test it out before fully committing to using it. We added the Grit integration to our github repo and manually migrated a few files to give it a baseline to go off of when generating new types. We used the first file from our migration plan as the starting point for it.
Grit first starts by inspecting the codebase and finding the files that need to be updated. This was pretty straightforward as we just pointed it at a folder with a couple small js files in it and told it to transform them into typescript. Next Grit analyzes the code and creates a branch to make the necessary changes on. Once it’s done making changes, Grit then uses our CI pipeline to ensure that changes pass tests and build process steps. Lastly it re-evaluates the generated code and modifies it based on any past review rules (more on these later) before creating a PR for our eng team to review.
In the initial PR it created, Grit added new types and interfaces based on the input and outputs of functions in the file and didn’t use any generic fallback types or leave any ts-ignore comments to prevent type errors.
The initial changes it generated were better than we expected and while it wasn’t perfect we were able to leave comments on the PR instructing it to not use certain syntax, update a type, move code to another file, and many other changes. It saves these as “review rules” that it uses to ensure it maintains a consistent output style when generating code. It also updates the PR based on the comment feedback. It sometimes took a few comments for it to narrow in on exactly what we were looking for, but this was expected of an AI coding tool at the time.
With the success of the first test, we went back to our requirements to make sure it was the right tool for us.
Grit generated sufficient types that improved our type safety and continuously generates migration PRs as they are merged in.
Grit can generate changes for any number of file batch sizes and uses our migrations strategy as the guide for what files it transforms next.
The changes Grit makes evolves with our use of typescript and it can be used to update past types to meet our current standards.
Lastly because it runs automatically it doesn’t require a dedicated engineer to ensure the migration makes progress. It integrates easily into our normal workflow allowing us to continue working on impactful customer features while also leaving feedback and approving migration PRs.
The team agreed that Grit was the tool that best fit our needs at the time and we committed to moving forward with it for our migration process.
Keep PRs small for quicker and easier reviews.
Initial file migration batches were too big and led to very large PRs slowing down progress.
Large PRs led to some bugs being missed and regressions being introduced by the tool.
AI tools are great productivity boosters but changes need to be thoroughly reviewed to prevent unnecessary / erroneous changes slipping through.
Ensure you have a solid migration plan before starting the process.
This greatly sped up the time it took for the migration to complete and gave us useful types early on that we could use in new code.
In the time between when we started our code migration and now the tools available have improved considerably. If we were to do this same evaluation today I believe we would end up with a similar methodology, but would likely use a model like Claude or Copilot to do the code transforms for us. These models could easily be integrated into a workflow that would batch transform files into typescript based on our migration plan. Models like Claude are also able to integrate into Github giving us the ability to leave comments and have the model make updates to the PR based on them. While it would take a bit more upfront work this would give us a result similar to what we got with Grit.
The migration of an entire codebase from one language to another is no small feat, but through proper planning and using the best tool for our team we were able to complete it in a relatively short amount of time. Moving to typescript eliminated a class of bugs from our frontend application and gave the team much more confidence in their work, and our customers and more reliable product.
The best tool for a job can vary greatly based on the wielder and the task at hand. At Found we pride ourselves in taking big initiatives, breaking them down into smaller steps, and working together to achieve them.
Found is a financial technology company, not a bank. Banking services are provided by Lead Bank, Member FDIC.