When Brendan Eich created the very first version of JavaScript for Netscape Navigator 2.0 in merely ten days, it’s likely that he did not expect how far the Slack Desktop App would take his invention: We use one JavaScript code base to build a multi-threaded desktop application, routinely interacting with native code, targeting Windows, macOS, and Linux.

Managing large JavaScript codebases is challenging — whenever we casually pass objects from Chrome’s JavaScript to Objective-C just to receive a callback on a different thread in Node.js, we need a guarantee that the individual pieces fit together. In the desktop world, a small mistake is likely to result in an application crash. To that end, we adopted TypeScript (a statically typed superset of JavaScript) and quickly learned to stop worrying and love the compiler. It’s not just us, either: In the 2017 StackOverflow Developer Survey, TypeScript was the third most-loved programming technology. Given how quickly static type checking is gaining traction, we wanted to share our experiences and practices.

Static Analysis to the Rescue

In the past, we used JSDoc to document our function signatures, using comments to inform code wanderers about the purpose and proper usage of classes, functions, and variables. This isn’t without its problems. Looking at the code, it’s hard to know what a JavaScript promise resolves with. You have to trust that the person who wrote the code documented it correctly and that people who changed it later correctly updated the documentation. In complex systems with countless modules and dependencies, it’s entirely possible to break a function without ever opening the file it lives in.

To improve our situation, we decided to give static type checking a shot. A static type checker does not modify how your code behaves at runtime — instead, it analyzes your code and attempts to infer types wherever possible, warning the developer before code ships.

A static type checker understands that Math.random() returns a number, which does not contain the string method toLowerCase().

To be more explicit, the user of such a type checker can support the system by manually declaring types — to inform both human and machine how the program is supposed to behave. The code below defines an interface for a “user” object and a method that is supposed to get the user’s age. A static type checker is able to analyze this code and warn about typical human errors, like expecting a possibly undefined property to always be there.

Interestingly, the code is not modified at runtime, meaning that a static type checker introduces no overhead for the end user. The above example at runtime looks like vanilla-flavoured JavaScript:

A smart static type checker increases our confidence in our code, catches easily made mistakes before they are committed, and makes the code base more self-documenting.

Porting Slack Desktop’s Codebase to TypeScript

We decided to use Microsoft’s TypeScript, which combines static type analysis with a compiler. Modern JavaScript is valid TypeScript, meaning that one can use TypeScript without changing a single line of code. This allowed us to use “gradual typing” by enabling the compiler and the static analysis early, without suspending work on critical bug fixes or new features.

In practice, switching the analysis and the compiler on without changing code means that TypeScript will immediately attempt to understand your code. It uses built-in types and type definitions available for third party dependencies to analyze the code’s flow, pointing out subtle errors that went previously unnoticed. Wherever TypeScript cannot understand your code, it will assume a special type called “any” and simply move on.

Our original plan was to slowly port files over, extending our vanilla JavaScript with more concrete type definitions wherever possible — adding interfaces, defining class methods as either private or public, and declaring enums. Along the way, we made two surprising discoveries:

First, we were surprised by the number of small bugs we found when converting our code. Talking to other developers who began using a type checker, we were delighted to hear that this was a common experience: the more lines of code a human writes, the more inevitable it becomes to misspell a property, assume the parent of a nested object to always exist, or to use a non-standard error object.

Second, we underestimated how powerful the editor integration is. Thanks to TypeScript’s language service, editors with an autocomplete function can support the development with context-aware suggestions. TypeScript understands which properties and methods are available on certain objects, enabling your editor to do the same. An autocomplete system that only uses words in the current document feels barbaric afterward. Gone are the days we find ourselves on Google, checking yet again which events are available on Electron’s BrowserWindow. Plugins are available for Atom, Visual Studio Code, Sublime, and nearly every other editor out there. Being able to validate our code without leaving the editor boosted our productivity immediately.

Looking ahead and thinking about code maintenance, we appreciate the ecosystem around TypeScript. As heavy users of React and the Node/npm ecosystem, the availability of type definitions for third-party libraries is a huge plus. Many of the libraries we import are already TypeScript compatible. If definitions do not ship with the module itself, they are likely to be found in the fantastic DefinitelyTyped project. React, for instance, does not ship with type definitions, yet a simple npm install @types/react installs them with no further configuration required.

TypeScript was such a boon to our stability and sanity that we started using it for all new code within days of starting the conversion. It’s taken about six months to annotate most of the JavaScript in the desktop app code base.

Committing with Confidence

To enforce readability and maintainability, all staged code is automatically checked with TSLint as a pre-commit hook — meaning that before Git commits your changes, it will first check whether or not the changed code passes our linting rules. We disallow the use of the “implicit any”, meaning that we now require all of Slack Desktop’s code to explicitly state the type wherever TypeScript cannot automatically infer it.

When the time comes to push a branch, Git first runs the whole codebase against TypeScript’s compiler, which analyzes the whole code base for structural and functional errors and transpiles modern features like async/await into ES2016-compatible code. By the time a Pull Request is opened, we already have the confidence that the structural dependencies within our code are sound.

It Might Look Scary

To us, the benefits of TypeScript dramatically outweigh the downsides — which do exist. Most notable to us is the additional training cost. Developers who have experience using any strongly-typed language usually pick up the syntax within an hour or two, but a file that makes full use of all of TypeScript’s features can look daunting to developers with a vanilla JavaScript background.

The most obvious solution to that problem is to phase features in slowly — you can simply enable TypeScript without changing any code, add some simple type declarations, and save more complex concepts like inheritance, generics, and advanced types (intersection types, mapped types) for either specific modules or a later stage. In the end, our experience is that one can reap a lot of benefits with the most basic use of TypeScript.

Giving Back

At Slack, we strive to be good open-source citizens. To that end, we strive to make the move to TypeScript easier for other developers: Whenever we find gaps, we try to close them.

Most notably, Slack’s own electron-compile allows developers of Electron Apps to write in TypeScript without having to worry about the compilation itself. RxJS, a Reactive Extension library heavily used at Slack, Netflix, GitHub, and many other companies, made the move to TypeScript with Slack’s support. The many small libraries written by our desktop engineers are all slowly gaining TypeScript support (like spawn-rx, electron-spellchecker, electron-remote, electron-notification-state, or electron-windows-notifications).

To get started with TypeScript, check out the official handbook. If you’re wondering what a small port looks like in practice, consider taking a peek at the port of spawn-rx. If you’d like to write your first lines of TypeScript for an Electron app, consider the excellent electron-forge, which implements electron-compile and supports TypeScript out of the box — it even comes with an excellent React/TypeScript template, which is an architecture we in the Slack Desktop Team love dearly. If mixing modern web technologies with native code to build a cross-platform desktop app sounds like an exciting mission to you, come work with us!