Evolution of the FindHotel Front-end Tech Stack

At FindHotel, we recently completed the migration of our front-end code from Facebook’s Flow to Microsoft’s TypeScript. We thought now would be a good time to detail how we did so without disrupting service to our users, and with minimal code freeze on the engineering side. Since this was the second migration of a transpiled language to another, we will give a brief overview of the evolution of the front-end technologies over time, and our motivation for doing so.

In short, we’ve gone through two major shifts, first from CoffeeScript → Flow, and a few months ago from Flow → TypeScript, while React with Redux has remained our main front-end paradigm since 2015.

2015: Adoption of CoffeeScript

When I joined FindHotel in the summer of 2015, the FindHotel website was to be built from scratch as up until then FindHotel relied on a partnership with HotelsCombined that provided our website as a white-label solution.

Back then, we chose to adopt CoffeeScript as the front-end technology, even though it was never a massively popular JavaScript dialect like TypeScript is today, there were a few very appealing aspects for us.

Robustness

CoffeeScript made it harder to make mistakes - enforcing the famous Good Parts of JavaScript.

Efficiency

CoffeeScript allowed for modern programming paradigms that JavaScript didn’t support back then, such as arrow functions and other syntactic sugar that made it easier to write robust code quickly.

Ruby-like language

I started as the first front-end engineer, the other engineers were all back-end engineers writing Ruby. Since the syntax and paradigms between CoffeeScript and Ruby were quite similar, it was easier for all of us to make adjustments to the different parts of the codebase.

No technology lock-in

Since CoffeeScript “is just JavaScript” as they say, it would still be possible (with some caveats) to switch “back” to plain JavaScript at a later stage if needed. Back then, ES2015 supported via Babel transpilation did exist and was up and coming, but hadn’t gained widespread adoption yet.

2017: CoffeeScript → Flow

Over the next few years, Babel gained traction, which allowed modern JavaScript versions to be transpiled (much like CoffeeScript did) to better-supported flavours of JavaScript that runs in most browsers.

There were a few aspects where CoffeeScript started to fall behind the Babel supported flavours of JavaScript that pushed us towards making the switch.

Tooling

Since Babel by now was enjoying much wider adoption, tools and IDE support were much better than CoffeeScript’s. ES2015 and later iterations had by now adopted much of what made CoffeeScript great, and tools such as linters and formatters allowed for a much better developer experience and increased robustness.

Future perspective

It became clear the maintainers of CoffeeScript at that time considered the language more or less “done”, without any specific roadmap of making improvements where the JavaScript ecosystem kept adopting new language features (including many of the ones that we were attracted to in CoffeeScript). There were rumours of CoffeeScript 2.0 (which eventually got built), as well as multiple forked projects trying to address existing problems. This increased the risk of an already small community becoming fragmented, decreasing the chances of a unified development ecosystem.

Developer Adoption

Around the end of 2017 our front-end team grew to 5 full-time employees, and most new joiners would likely have production experience with ES2015, but not with CoffeeScript. On-boarding developers to CoffeeScript was never much of a problem, but there was still a slight learning curve. Additionally, not many candidates were particularly excited about CoffeeScript, while the shiny ES2015 was something most developers were eager to work with, making it easier for us to hire new talent.

Static Typing, Flow vs TypeScript

We were interested in adopting a statically typed flavour of JavaScript, which CoffeeScript seemingly wasn’t planning to ever add. We evaluated Facebook’s Flow versus Microsoft’s TypeScript, a few aspects swayed us towards Flow at that time:

Facebook backed: since we used React extensively, we figured since Facebook uses Flow internally for the React codebase, we assumed as long as Facebook supports React, Flow would be supported and maintained too.

Meanwhile, I myself was distrustful of adopting a Google pushed (with Angular, at that time) JavaScript dialect, as I’ve seen Google push and switch strategies many times, with Google Closure and Dart.js with neither of them settling on becoming an industry standard, while Angular was going through a few major versions that broke existing codebases.

In contrast, Facebook has a great track record with evolving React gradually without many breaking changes (luckily things got better with Angular's approach, too).

Gradual adoption: since we were converting an existing codebase, we would need to be able to add types gradually. Back then, TypeScript only just started supporting this. Flow was basically JavaScript with a few type definitions added, while TypeScript was more like another language altogether (by adding new languages constructs), which posed some risk of the same faith as CoffeeScript with tooling falling behind “real JavaScript” (I was wrong on this regard, as evident with TypeScript’s massive popularity and excellent IDE and tooling support - more about that later).

The transition strategy

As mentioned, we aimed to transition the codebase without instituting any downtime of our product, or a long code freeze for our front-end engineers. There were multiple tools available to convert CoffeeScript to reasonably clean and readable JavaScript.

We set up a multi-stage conversion plan where we created a conversion toolkit branch that was developed separately from the main master branch, in which we:  

  • used decaffeinate to mostly automate the conversion process
  • wrote custom “codemods” with JSCodeShift to clean up any post-conversion JavaScript patterns we didn’t like

We then converted a copy of the master branch called master-js  to perform QA, and once we were happy, deployed it to production. We then applied the toolkit branch on each open Pull Request and merged it into master-js and wrote new code directly in JavaScript.

Once all open pull requests were merged, we started gradually adding Flow types to the codebase.

2020: Flow → TypeScript

While we were happy with the static typing features that Flow gave us, fast-forward a few years, and we ended up with another major transition - this time to TypeScript. Over time, TypeScript become many  orders of magnitude more popular than Flow. At this point Flow is still maintained and being evolved by Facebook, but at a much slower rate and with a less predictable roadmap that Microsoft is following with TypeScript.

To sum up, our main reasons for moving away from Flow → TypeScript were:

Predictable roadmap

Facebook maintains and evolves Flow mostly based on what is important for them internally, without ways for the wider community to influence the roadmap. TypeScript follows a predictable release schedule and accepts 3rd party pull requests.

Better tooling / IDE support and performance

Over time, with massive investment from Microsoft in the VSCode IDE, the tooling around the TypeScript ecosystem is much vaster than Flow’s.

We encountered many instances where the Flow server would crash and have to be restarted to enable type support in our IDEs. Recently Facebook has dropped support for Atom, while IDE support for TypeScript keeps improving steadily.

Developer adoption

When I attended the Advanced React Conference in the winter of 2019 with two of my colleagues, there was a request for show of hands between engineers who were using Flow vs TypeScript in production. I believe we were about the only three people raising our hands for using Flow :| While Flow was still mostly working for us, its future looks quite bleak compared to TypeScript’s. An interesting pointer of TypeScript’s success over Flow’s for us was when Facebook’s testing framework Jest switched from Flow to TypeScript to facilitate external contributions from the community.

Here are the NPM downloads for Flow vs TypeScript over the last 5 years (https://www.npmtrends.com):

3rd party library typing

Due to TypeScript’s popularity, 3rd party libraries and packages generally use TypeScript, thus consuming them in a type-safe way is much easier with TypeScript as there are more types available than for Flow. This reduces the number of type errors in our codebase when consuming external libraries.

The transition strategy

After our successful conversion from CoffeeScript → Flow, we opted for a similar strategy to convert from Flow → TypeScript, where we prepared the automated conversion in a toolkit branch that can be applied on open branches. The transition was easier this time around as the two typed languages are so similar, though there were still a few files that needed manual fixes before the conversion would work automatically.

To recap, we:

  • used flow-to-ts to convert Flow to TypeScript automatically
  • wrote custom Makefile commands to clean up unsupported patterns - no need for JSCodeShift this time around - and a script to make git understand files were changed and their extension renamed, should be considered “git moved”.
  • applied XO linter fixes to force a consistent coding style across the codebase
  • plan for manual clean up after the conversion of a few patterns that Flow supports but TypeScript doesn’t, such as opaque types

Bumps in the road

We did run into a few snags, mainly with Github since we were changing our codebase of nearly 4000 files at once.

  • Github doesn’t like Pull Requests with 3000+ files, so manual review had to be done offline - our test suite helped us with the confidence in shipping the converted files
  • Github doesn’t detect git moves between .js(x) → .ts(x) properly, meaning we resorted to comparing local diffs for open pull requests. This was not a problem with git itself, rather with Github: https://github.com/isaacs/github/issues/900

In the end, these didn’t end up being a massive pain, and we were able to move pretty smoothly to TypeScript.

The aftermath

A few leftovers remain that we’re addressing with a campground-rule strategy over time:

  • FlowTodos: converting to TypeScript removed quite a lot of gaps in the types of external packages, but some “pragmatic” (lazy) pieces of code might still be improperly typed; in the meantime we created a global TypeScript alias for us to know whether we really intend for something to be unknown or whether it’s a hack that needs fixing
  • Re-introduce opaque types: we didn’t have many opaque types, and this is not something we’ve solved for yet, though there are some solutions available we’re considering
  • Interfaces vs Types: The flow-to-ts tool we used converts everything to type instead of interfaces, as this is what Flow uses. Since types and interfaces can be mixed we intend to write interfaces for new code

Conclusion

It wasn’t trivial, but overall we’re very happy with our move from Flow to TypeScript; type coverage increased, and there are no more performance issues in our IDEs. We are as always grateful for the open-source community in providing with the tools to automate (most of) the conversion with a good degree of confidence, and are excited to be part of the TypeScript community!

PS: I apologise for including a graph in this post, I am sure you're tired of seeing them after the past year. That said, we're lucky to be doing well even in these troubling times, and our internal performance graphs are looking pretty good:

We are hiring for many roles, so if you’re looking to join an innovative travel company that can overcome tough challenges, have a look at our career page and do reach out! https://careers.findhotel.net/