Over two years ago, the Wealthfront web team began adopting TypeScript to take advantage of the type safety and related functionality it enables. This is a common migration in the past few years across the industry since the web community mostly settled on TypeScript as the language of choice.
Since then, the codebase has naturally migrated towards TypeScript as engineers converted the code they touched, and all new code was written in TypeScript. After two years, the codebase was about 35% TypeScript by file count. This was great progress and helped increase the team’s confidence in adopting and using TypeScript. Unfortunately, at this pace, it wouldn’t be complete for four more years.
Gradual migration was happening, but it was being impeded. We set out to remove points of friction to expedite this migration and realize as many of TypeScript’s benefits as soon as possible. In the end, renaming the remaining files removed much of the friction and provided many of the benefits.
Obstacles
The first obstacle we commonly faced was renaming existing files from a JS/JSX extension to a TS/TSX extension. This was simple but required restarting the local development server which can take several minutes.
Second, renaming a file would then introduce countless new type errors, which now had to be fixed. This could be a time-consuming process depending on the complexity of the types. It may also require converting more files, or changing existing types in other files. In turn, this could create more type errors, ending up in a rabbit hole fixing type errors.
Finally, the full potential of TypeScript’s functionality was not being realized with two-thirds of the codebase still in JavaScript. TypeScript can infer types from JavaScript files, but it’s not perfect. This means things like autocomplete and strong types often weren’t possible. On the flip side, strongly typed global utilities and components that were written in TypeScript, weren’t being type-checked in JavaScript files.
Converting all the JavaScript files to TypeScript files wouldn’t provide all of the benefits TypeScript has to offer, but it would eliminate most of these friction points. It removes the need to restart the development server, allows improving just one type at a time, and unlocks additional TypeScript benefits like stronger type safety, better inference, easier navigation, and improved autocomplete.
Migration
We are certainly not the first to set out on this journey from TypeScript to JavaScript, much of this inspiration was drawn from many others who have done this.
The majority of the migration was done with Airbnb’s ts-migrate tool which was built specifically for this use case. It contains several commands to perform different tasks as part of the migration.
First, the tool provides a rename command to convert files from a “JS/JSX” extension to a “TS/TSX” extension. However, doing this will introduce many new type errors.
The next step is to fix (or ignore) all of these type errors so the project successfully compiles. This tool contains a set of plugins that modify the code (codemod) to resolve as many of these errors as possible. Any remaining errors are then ignored so the project successfully compiles.
Process
There were two approaches considered, migrate the codebase all at once, or in iterative chunks.
An all-at-once approach would likely require a code freeze which requires additional coordination but can avoid issues like merge conflicts. Any potential issues become much harder to debug and revert.
Migrating in chunks can be performed on smaller pieces of the codebase which can be more easily coordinated to avoid merge conflicts, reviewed, and merged. If unexpected issues do arise, it’s a significantly smaller search area. For these reasons, the codebase was migrated in several chunks each day over the course of a week.
The first step was to run the ts-migrate rename
command on a subdirectory of files to change the file extension.
Then, the ts-migrate migrate
command was run on the same subdirectory which runs several plugins. These plugins attempt to fix as many type errors as possible by adding missing class properties for legacy React refs, converting React prop types to TypeScript types, adding explicit any
keywords for missing types, or in the case of some type errors casting the value to any
. Any remaining errors were then ignored with the @ts-expect-error
directive.
After, Prettier was run over these files to format them consistently, which sometimes caused issues that required re-running the migrate
command. Finally, any new ESLint errors also had to be fixed or ignored.
This tool effectively converted the entire codebase to TypeScript files, but as a result, added many any
types and ignored many errors with the @ts-expect-error
directive.
Most of these types and errors require a human to read and understand the code to properly fix.
Further improving type safety
With the entire codebase fully converted to TypeScript, our effort can now be spent incrementally improving type safety. To assist with further improving type safety, a command-line tool was added to aggregate this information to surface opportunities and fix systemic typing problems.
It counts the total number of any
keywords and @ts-expect-error
directives across the entire codebase.
The following code is a simplified example of this tool that iterates through the relevant files and converts the source code into an abstract syntax tree to traverse and find the relevant nodes (any
keywords and @ts-expect-error
directives).
In addition to totals like this example, the command-line tool also aggregates by file name and error message so it’s easier to diagnose and improve systemic type issues.
This same tool can then be run across git history to generate a trend to ensure consistent progress is being made towards improving type safety over time by reducing the total number of any
keywords and @ts-expect-error
.
This is not a perfect metric for “type quality”, but given the current state of the codebase, it’s measuring the biggest opportunities for improvement. Once most of these type issues are resolved, a different measure of “type quality” will likely need to be established.
Conclusion
While converting the entire codebase to TypeScript files is only a milestone along the journey of a strongly typed codebase, it’s an important step to maintain and improve the quality of the codebase and product over time. In our case, it proved to be one of the biggest obstacles in the way of the gradual migration towards stronger type safety throughout the web codebase.
Disclosures
The information contained in this communication is provided for general informational purposes only, and should not be construed as investment or tax advice. Nothing in this communication should be construed as a solicitation or offer, or recommendation, to buy or sell any security. Any links provided to other server sites are offered as a matter of convenience and are not intended to imply that Wealthfront Advisers or its affiliates endorses, sponsors, promotes and/or is affiliated with the owners of or participants in those sites, or endorses any information contained on those sites, unless expressly stated otherwise.
All investing involves risk, including the possible loss of money you invest, and past performance does not guarantee future performance. Please see our Full Disclosure for important details.
Wealthfront offers a free software-based financial advice engine that delivers automated financial planning tools to help users achieve better outcomes. Investment management and advisory services are provided by Wealthfront Advisers LLC, an SEC registered investment adviser, and brokerage related products are provided by Wealthfront Brokerage LLC, a member of FINRA/SIPC.
Wealthfront, Wealthfront Advisers and Wealthfront Brokerage are wholly owned subsidiaries of Wealthfront Corporation.
© 2022 Wealthfront Corporation. All rights reserved.