3 min read

Predictable programming 1: how Typescript isn't Rust

I've spent a lot of time building and maintaining React applications. React, with Typescript, is fine, but I keep coming back to the same realisation: it's not Rust. Over the past few years I've been learning Rust, and it has transformed how I write and think about code. However, as I still have to write React, I've been spending a lot of time trying to emulate the benefits of Rust in Typescript.

This is the first part in a series of posts about a "style guide" of sorts for writing Typescript in a way that feels a lot more like Rust. I focus especially on predictability[1]: the minimisation of undefined behaviour and other known unknowns in my codebases.

What makes Rust predictable?

In programming we often talk about the happy and unhappy paths. Rust is the first language I've used that requires you to explicitly define both. Not only that, but Rust also focuses on good ergonomics for doing that.

The most common example is error handling. In other languages, we're used to seeing throws and catches, which allow you to implicitly ignore an unhappy path and assume that the parent scope will deal with it. Rust handles that entirely differently: any errors that could happen must be explicitly declared as part of the return type with a Result<T, E>. You can still ignore an error and pass it to your parent, but it requires you to choose to do that.

However, there is another important way that Rust encourages covering all paths exhaustively. Rust recognizes through enums that many variables carry not only their value, but also a state. With the match expression, you can then force yourself to handle each state explicitly. This provides the ergonomics for the programmer to define and traverse complex code paths, without the mental overhead of implicit state management.

This principle can be applied beyond just Rust's syntax into development more generally. State machines allow you to strictly model component behaviour; strong typing makes many of these implicit semantic differences explicit; opting in to explicit exception passing allows you to capture almost all possible errors at compile time.

Why not use Rust?

Wait, you might ask, can't you use Rust in the frontend now? And that's true! I could use something like Yew or Dioxus to write my web applications, and I could continue building in React Native (or Swift and Kotlin separately) and maybe use something like Crux for all the shared business logic.

However, I highly value a mature ecosystem, especially for the frameworks you use. I get the most use out of a framework or tool that I don't have to debug myself. I'd much prefer to use a commonly used framework for something as complex as UI[2], over a new thing that might break in ways that require a lot of effort on my part to debug.

Next up

I'm lining up two more posts in this series: one that is more of an experimental style guide on how I write Typescript (which will also be available as a living document on my homepage), and another specifically on using state machines in React through XState. Additionally, there's a post adjacent to this topic on making errors in Typescript better.

If any of this interests you, be sure to sign up for email notifications, add my blog to your RSS app or what have you, or follow me on Twitter or Mastodon.

  1. I've been calling this "predictable programming" (as seen in this post's title), but I don't know that I actually like the name, and friends have said they don't. Feel free to yell better names at me on Twitter. ↩︎

  2. Honestly, I feel a lot more confident about using something relatively new in the backend. I'll be running it in a Docker container, so the environment is stable. Frontends run on all sorts of different devices, browsers, and systems, each with their own weird limitations. I don't want to be dealing with all that. ↩︎