Static Analysis to the Rescue
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.
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
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.
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.
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
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.
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!