Open Sourcing splitshot: TypeScript and CoffeeScript, Together

Hulu Tech
Hulu Tech blog
Published in
5 min readDec 10, 2019

--

By Sean Barag, Senior Software Developer

For fans of: programming languages, javascript, typescript, coffeescript

It’s been a while since we’ve talked about our Living Room application on this blog, but that’s because big things have been happening! Last year, Kirk Yoder described how a unified runtime allows Hulu to deploy a single codebase across multiple clients. That’s still very much the case, so let’s spend some time a little bit further up the stack, in the JavaScript source itself.

The short version: For living room devices not including Roku or Apple TV, Hulu used to be written in CoffeeScript, but JavaScript has advanced since then. TypeScript’s advanced compile-time guarantees help Hulu catch errors before customers find them, but doesn’t help with code written in CoffeeScript. We’ve open-sourced a tool that generates TypeScript definitions from CoffeeScript called “splitshot.”

Splitshot allows us to write new features with TypeScript’s most strict settings without having to modify any of our existing CoffeeScript code. Bridging that gap gives us significantly more confidence in the stability of our code. Simply put: we’re able to keep our coffee(script) but sleep a little better at night.

The Before Times

When Hulu was developing its first set of JavaScript applications on that platform, JavaScript was a pretty different language than the one we know today. Variables were only declarable with var, classes existed only via verbose, immediately-invoked function expressions, and Ruby was extremely popular thanks to Rails. We started writing our app in CoffeeScript — a language that compiles down to pure JavaScript — due to its support for classes, lexical scoping, and “fat-arrow” binding of JavaScript’s this variable.

A simple CoffeeScript program that utilizes classes and “fat-arrow” bindings.
The equivalent JavaScript produced by the CoffeeScript compiler.

These features wouldn’t be standardized in vanilla JavaScript until 2015’s ECMAScript 6 standard, and even then would require a very modern JavaScript environment. CoffeeScript was our life-blood, and its terseness allowed for some very rapid iteration.

Pushing the Limits

We maintained that codebase through several large overhauls, including a migration to the custom, open-source data-binding library that Zachary Cava talked about last year. But eventually, we began to run up to the limits of large CoffeeScript applications — we had over 600 CoffeeScript files at one point:

  • CoffeeScript’s existential checks (aka the safe navigation operator) were extremely handy, but didn’t force us to handle the resulting null values
  • Modifying a function in file A that called a function in file B required both files to be open to ensure the call signature was correct
  • Some of our most common JavaScript errors were reported as “undefined is not an object” or “undefined is not a function.” While accurate, those messages didn’t help us prevent errors before users found them.

Now late in 2016, TypeScript’s compile-time guarantees were gaining popularity in the JS community. We’d been interested in getting some of our code written in TypeScript, but we quickly realized a limitation imposed by our existing CoffeeScript source.

TypeScript can provide its most useful guarantees by enabling the — strict family of arguments, which require type information for non-TypeScript code being called. The TypeScript compiler didn’t have any information about our CoffeeScript files, so it could only help us when TypeScript files called code from other TypeScript files. With 600 CoffeeScript files in existence, hand-writing declarations for those classes would require either a “stop the world” approach — where all code is locked while we hand-write declarations — or a “please make sure you update the declarations” approach — where we all eventually forget to update them. Neither is particularly attractive, especially while attempting to launch a major UX redesign.

“Is this a dumb idea?”

Our solution started with a few lines of JavaScript and a message to my manager:

“Is this a dumb idea? I feel like this is a dumb idea.”

Since the CoffeeScript compiler exposes its parser, it’s possible to gain access to the abstract syntax tree that the coffee binary would use to generate vanilla JavaScript. Since a CoffeeScript class and all of its members have equivalent constructs in TypeScript, would it be possible to parse all of our CoffeeScript and generate .d.ts files for each?

I spoiled that in the title I guess, but that’s exactly what we did. “splitshot,” the tool we wrote to handle this, attempts to understand what each CoffeeScript file exports via module.exports and generates a declaration file for each one automatically.

Splitshot takes CoffeeScript like this…
…and generates TypeScript declarations like this!

Aside: why “splitshot”?

Hulu’s LivingRoom team is based in Seattle, a city that prides itself on coffee culture. A splitshot from a coffee shop is a modification to any espresso-based drink in which one of the two shots is pulled from decaffeinated espresso. Just like this tool, they let you keep your coffee(script) but sleep a little better at night :)

Concessions, but not the “get your popcorn here” kind

A few complications immediately came up while attempting to generate strict-mode declarations:

  • Function arguments can be multiple types or even excluded
  • Null is sometimes allowed, but sometimes not
  • CoffeeScript’s default value support makes this all a little more tedious!

To accurately capture the dynamic nature of JavaScript/CoffeeScript in TypeScript declarations, we had to make some concessions:

  • Most properties and class members are declared as any
  • All functions return any
  • All function arguments are optional
  • All function parameters are of type any

While that slightly diminishes the type-checking abilities of the compiler, it allowed autocomplete in any editor with a TypeScript plugin, and forced all TypeScript to TypeScript interactions to be as strict as possible. We didn’t gain any new confidence about the CoffeeScript we had, but we did gain a lot of confidence in the consumers of that code. Since these declarations are automatically generated for every new build, we’re able to detect potentially breaking changes just by compiling our application.

Next steps

Hulu has been using splitshot for our production app for just about two years. It strives to be a transitional tool to help decrease a codebase’s reliance of CoffeeScript while encouraging type-safe TypeScript code. To that end, it’s been a resounding success! - -strict has been enabled for all of our builds over that period, and we’ve noticed a sharp downturn in invalid property access bugs. (We’re also actively replacing some of our most core CoffeeScript modules with native TypeScript implementations, but that’s a blog post for another day!)

In the meantime, @hulu/splitshot is available for installation via NPM, and the source is available on Github. We welcome GitHub issues for any bugs you might find and would be happy to talk about this kind of migration in the comments below.

If you’re interested in working on projects like these and powering play at Hulu, see our current job openings here.

--

--