ArticlesEngineering

Learning to like types

The considerations that took me from being opposed to type systems to enjoying (some of) them.

After writing software for quite some time and having worked the end-to-end of many projects in a full-stack capacity, I held a pretty strong belief: "All types are bad, skip 'em."

What made me dislike types so much?

  • Verbosity: I'm literally writing double or triple the code I need to accomplish my goal when I use types.
  • The coupling between types and object-oriented (OO) programming: the languages where you can't have types without tons of internal state, pointless class hierarchies, and a lack of meaningful expressiveness.
  • Lack of openness: I can't use typed software in the ways the original author didn't foresee. I'm trapped for what I view as arbitrary reasons.
  • Lack of conviction: You're telling me to deal with all the above and then I'm dealing with spooky un-typed garbage anyways? (Like how anything can be Object in Java or any in Typescript) You want me to play the game that you admit doesn't work?

Something that was interesting to me was, despite not liking types of any sort, I carried a significant mental overhead around them. Why? Compiler and interpreter optimization: knowing how variables are used leads to more efficient programs. Notably in JavaScript, duck-typing was off the table for any serious work–V8 choked on redefining a variable to multiple types. I felt smart keeping track of these things in my head.

Bad typing

Another thing that made me dislike types so much was the culture and feedback loops around them: shit code begets shit code. Some examples:

Verbosity

We get antiquated to verbosity in a codebase: "I expect that to accomplish <task> I'm going to need X% non-reducible boilerplate." Slowly, perhaps, but surely X increases and we're full steam ahead in what I refer to as "pasta town."

Coupling to OO

Similarly if you're given an object-oriented interface that pulls a few tricks: internal state, inheritance, magic dependency-injection you have two paths:

  • Fight this interface with functional programming and take 3X times as long to get the job done and it's done well.
  • Keep fanning the flames: get the job done poorly, sprinkle more hacks atop, and let the next person suffer.

Lack of openness

The first time a dev faces the friction of not being able to use something that is almost a perfect fit, they are distraught. They are forced to duplicate the code, but don't get scolded. The next time they do it, they feel fine. "This is life."

Lack of conviction

We embrace more and more clever tricks in the abuse and skirting of type systems. "We require typed code" meets a deadline and then it's "We require typed code except for this bit over here cuz–" and it degrades to "use types when it's convenient for the author." Great, now we get to deal with typing ": Object" over and over without gaining any additional safety.

I joined Stripe holding all these underlying beliefs that types were bad. I joined a team with 2 ex-Java engineers no less. We were all forced to write Ruby and use Sorbet for type-checking. For me it was, "yuck, types." For the others it was, "yuck, no types and bad tooling." (Sorbet was essentially a CLI script at that point. We were years away from auto-complete and go-to-definition.)

Forced to use a typed language, my gut led me to using as little of it as possible. Find a way to leverage types in order to check the "it has type-checking" box and not fall into these bad things I'd conflated with type systems in general. How did it go?

Good typing

I love types now! I'm picky about what types I find valuable, and types are a super-power when wielded properly. Let's work backwards through my previous issues with types:

Lack of conviction

Sorbet was novel to me as its type system operates at two levels: static, "compile-time" checking AND runtime checking. Sorbet was designed for gentrifying years of untyped code. Littering wrong type annotations on existing code helps no one.

Adoption was incremental, but the run-time type-checking made all the difference. There was no magic Object/any escape hatch tricks you could throw at the type system. If you cheated on the static time checks, you got caught on it in production with an exception. There was no ceremony for the sake of ceremony, in fact when you put a type on something you needed to be damn sure it is what you say it is. We could trust nothing else.

Lack of openness

Types limit what you can do: you can't re-use code if the author did not intend it. I thought this was a bad thing but it's a god send. Using code in ways it wasn't intended is a good 80% of the holes we as infrastructure / platform teams dig out of. Types let us stop some of those holes from even forming? Sign me up!

When something is almost a perfect fit, it is not a perfect fit. You want to know that. You want to be making a conscious choice to either extend something to meet your needs or build something yourself. Over-extension can be deadly for velocity. You don't want to walk into it blind.

I'll also emphasize the importance of having solid, simple primitives that are general purpose. For example, if one service is the only one that has rate-limiting, instead of asking, "dang how can I get my code on that service to take advantage of the rate-limiting?" Ask, "can we make some general-purpose rate-limiting tooling? I'd like some too." Worry less about code size and worry more about simplicity and coupling concerns.

Coupling to OO

Don't worry, OO was trash and still is. You can use types without it. It's just a matter of how hard the language makes it for you:

  • Java: yep, it's garbage. There's just no separation. I won't consider Java a real typed language until it gets strict nullability and its new record syntax for lightweight data classes. (Other JVM languages do a better job.)
  • Ruby/Sorbet: Ruby is still class based but you don't have to use those parts. Lean on static methods, modules, and T::Struct.
  • Typescript: {a: number} Beautiful! So little boilerplate that I can just talk about data with minimal ceremony.

Sadly, people will fall into traps here. Get comfortable with using only 10% of the total language and plan on needing a strong mentorship and code review process to keep from straying down the OO path. I think solving this problem of how to get a popular language without the 90s/00s cruft of OO and its influence with types is ongoing. The benefits of types are there, so be patient and vigilant.

Verbosity

Types are more typing. There are ways to reduce it but it's never zero. What's this? Oh it's–

TRADE OFFER ALERT

You give me: writing expressive and useful types

I give you: an order of magnitude less unit and integration tests to write, more self-documenting code, and immediate feedback via IDE tools.

I have done the grind with no types. I know for good software we're trading off time spent writing tests for time spent writing types. You can't take just any old types like the crappy ones you're probably used to and say they are better than tests. But think of runtime enforced types: why am I wasting the time to write, "X does this when given a number / string / etc" style tests when types can outright prevent the ones which don't make sense.

The expressive, code-local nature of types overweighs the value of far-off, non-exhaustive tests. I'm going to be spending time on one or the other and I'd rather work to make types worth the investment.

Okay, so to summarize:

  • Types are good? Maybe! I can't defend Java, but there are type systems that are valuable to development that you ought to be participating in. The best I can do in this piece is take someone from absolutely anti-type to "hmmm..."
  • Incorrect types are useless. Better type-systems, which I offer as the example runtime type-checking, allow us to prove they are correct where it matters: production.
  • Verbosity due to types is non-zero. It is more to write but there are systems where instead of simply adding more work it reduces the work to be done in other areas (e.g. tests).
  • Yep, object-oriented code is still bad. Strong focus on simplicity and decoupling remain essential.