Converting Codecademy to TypeScript 2: Technical Changes

Josh Goldberg
Codecademy Engineering
6 min readMar 30, 2020

Previously: Part One: Converting Ourselves

Once we knew we were going to convert our main “monolith” repository from JavaScript to TypeScript, we had a few technical hurdles:

  • Strategy: Would we convert all-at-once or incrementally?
  • Integration: How would TypeScript fit into our existing CI/CD systems?
  • Automation: What could we do to minimize work?

Adapt! React! Readapt!

Ryan Howard saying “Adapt, React, Readapt, Apt” to Michael Scott from The Office.

There is no one-size-fits-all solution for any shift. It would be foolish of us to strongly recommend any precise path to adoption without understanding your specifics. Your team has a unique blend of legacy code, business requirements, and prior understanding. Your strategy should adapt to it.

We made a few subjective technical decisions from the start based on our blend:

  • We did start with --strictNullChecks because the flag is just so useful, and our React code tended to be written without many violations of it.
  • We did not start with --noImplicitAny because it would require much more work upfront and is harder in a mixed JS/TS codebase; instead, we tabled that flag for later.
  • We did not fail pull requests that reduced type coverage percentages. Doing so inconveniences developers unfamiliar with TypeScript or in a hurry to modify existing, very-not-type-safe legacy code.

Your internal technological evangelism should probably work differently than ours. Convincing yourself and others of a choice to make is as much understanding your team members’ core motivations as it is marketing it. I’d personally recommend the classic How to Win Friends and Influence People by Dale Carnegie for broad tech evangelism strategies.

Strategy

Jim and Pam from The Office high fiving
Teamwork makes the dream work. [source]

Converting all of our files to TypeScript at once was unfortunately not a viable option. There were too many type checking complaints in our code to do a pure file rename, and ignoring them with // @ts-ignore comments would have polluted the code base without generating the appropriate types to write clean new TypeScript code.

We needed an incremental adoption with as few structural changes and as many automated conversions as possible.

See the previous post, Converting Ourselves, for our behavioral aspects — the remainder of this post is solely the technical changes.

Integration

Our tech stack is pretty common for large web applications made post-jQuery/Knockout. Its main pieces of technology are:

  • Webpack: compiles our source files (JavaScript, SCSS, etc.) into combined “bundles” that can be loaded by users’ browsers
  • Babel: used by Webpack to transpile fancy new or non-standard syntax in our JavaScript files into older equivalents so that users’ browsers are guaranteed to be able to run our code
  • JSX: syntactic sugar for JavaScript files that makes writing React easier
  • ESLint: separately run during our builds to check our source files for obvious defects without running them

Babel

Babel logo

Babel’s TypeScript preset allows your TypeScript files to be picked up by your Babel transpilation with their TypeScript syntax removed. The files are treated as regular JavaScript files thereafter. Perfect!

Our single babel.config.js change to add TypeScript was minimal, and involved us just adding @babel/preset-typescript to our list of extended presets.

Note that the Babel TypeScript transformer is speedy because it doesn’t perform TypeScript type checking. We added a tsc run as a separate build command to make sure we didn't introduce type errors.

You can peek at our shared open source babel-preset-codecademy if you’d like a reference. The babel.config.js is unfortunately not open source (yet!).

Webpack

Webpack logo

We use the standard recommended babel-loader in our Webpack configuration, which is what tells Webpack to run Babel transpilation on source files via the normal Webpack loader schema.

That meant our only effective change in Webpack was to switch the Babel loader from its default of including all JavaScript files to (j|t)sx?, or js, ts, and tsx files.

You can also peek at our shared open source webpack-config for a reference. The specific pull request is a little more complex, but visible here.

JSX

TypeScript understands JSX syntax as long as a file’s extension is .tsx.

💯

ESLint

ESLint logo

ESLint supports TypeScript files using the typescript-eslint project. We added typescript-eslint to our existing ESLint configuration and otherwise didn’t significantly change our existing lint steps.

ESLint’s only major hiccup was a dramatic increase in lint times that, while not the last step to finish in our parallelized build pipeline, was still an annoyance I learned by coincidence that the root cause was from including the community import ruleset; removing it fixed the lint times.

FAQ: Is ESLint Still Useful With TypeScript?

Yes!

We still use ESLint for our linting even though many of its core rules are made redundant when using TypeScript.

  • Many stylistic choices and technically-acceptable-but-practically-negative code patterns aren’t banned by TypeScript.
  • ESLint’s community rulesets also add a lot of value — for us, particularly around JSX/React accessibility. I couldn’t imagine writing React hooks without the rules-of-hooks ESLint rules!

Our shared ESLint configuration is also open sourced in our client-modules monorepo for reference.

FAQ: What about TSLint?

TSLint is a TypeScript equivalent to ESLint from before typescript-eslint was created. It works similarly to ESLint but with out-of-the box TypeScript support.

TSLint is dead. Stop using it. Stop using it!

Darrell from The Office crying softly
RIP, TSLint. [source]

Automation

In general: the less work you do in a conversion, and the more you automate it, the easier it will be. We knew we wanted to minimize work (and therefore pain) early on and were fortunate to work in an already semi-typed world thanks to our moderate usage of React prop-types.

Somewhat coincidentally, I had started work a few months earlier on a project called TypeStat: a utility that creates new and improves existing TypeScript type annotations in JavaScript and TypeScript code. Converting from JavaScript to TypeScript is a challenge many organizations face yet the existing converters I could find online were very minimal or limited to particular frameworks.

TypeStat was an absurdly useful tool in automating type migrations. It certainly didn’t do everything for us — Redux typings in particular were a pain to work though — but it was excellent at generating best-guess starting types for us to work off of. At the very least, the time spent working on it was worth the time it saved in declaring types.

If you’re in a situation at all like ours, I’d definitely encourage you to give TypeStat a try. It’s still early stage and buggy, so any GitHub issues or pull request activity would be lovely. 💖

Completion

Technical conversion work on our main codebase commenced on April 2nd, 2019: a day after our April 1st LOLCODE course launched. The conversion took us until we merged the last commit at 5pm on Tuesday, December 17th: 259 days since the technical effort began in earnest.

Screenshot of a Slack post exclaiming 100% TypeScript using emojis, with many happy emoji reactions.
Custom Slack emojis are a truly game-changing feature for team communications.

After the conversion finished, we ran a script to chart out conversion over time; the script created a .csv file for each commit to our master branch.

It used this line.sh to, for each commit, print a row with its timestamp, JavaScript file count, and TypeScript file count.

Visualized chart of 0–100% conversion progress, by file extension percentage, starting in April at and ending in December

Most of the time was spent doing small conversions, with the occasional giant batch conversion for fun.

When the conversion was completed, there were approximately 1,800 Webpack-consumed files in the Codecademy main repository. We also happened to spot several dozens of unused files during the migration, separately converted our Expo + React Native mobile app using similar strategies, and created or deleted many dozens more files. The precise number of converted files is probably a bit higher than 2,000.

Custom cake printed with the 100 emoji next to the TypeScript logo
We purchased a custom cake to celebrate. It was delicious.

--

--