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?
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.
Another thing that made me dislike types so much was the culture and feedback loops around them: shit code begets shit code. Some examples:
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."
Similarly if you're given an object-oriented interface that pulls a few tricks: internal state, inheritance, magic dependency-injection you have two paths:
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."
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?
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:
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.
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.
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:
T::Struct
.
{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.
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: