Types are a conversation, not a contract
I used to think strong typing was about preventing bugs. Now I think it's about thinking out loud — and the bugs prevented are a side effect.
I used to think strong typing was about preventing bugs. The arguments I made for typed languages were always defensive: refactors won’t blow up, regressions won’t sneak through, on-call won’t get paged at 3am for a null you forgot to handle. All true. None of those reasons are why I actually keep coming back to typed code.
I think strong typing is, primarily, a way of thinking out loud. The bugs prevented are a side effect.
The actual reason
When I write a type signature, I am writing the thinnest possible description of a thing — what it accepts, what it returns, what it’s allowed to be. The act of writing that description forces me to make decisions that I would otherwise defer to “I’ll figure it out when I get there.” It’s the same reason writing a one-page design doc clarifies thinking even when nobody reads the doc.
The TypeScript line:
type Subscription = {
id: string;
status: 'active' | 'canceled' | 'pending';
customerId: string;
cancelsAt: Date | null;
};
is doing more than declaring a shape. It’s a tiny argument:
- The id is a string (not a number — explicitly chosen, e.g. for Stripe interop)
- A subscription is in exactly one of three states (not “any string” — bounded)
- A subscription always belongs to a customer (not optional — required at construction)
- The cancellation date can be null (because most subscriptions aren’t canceled — and the type makes that explicit, instead of “missing field” or “empty string”)
Every one of those decisions is a thought I had to have. If the type was Record<string, any> I could have written the same code with the same data shape, but I would have made none of those thoughts explicit. Six months later, when I’m trying to remember if a subscription can be canceled-but-still-active, I’d have to read the code to figure it out.
Where this changes how I write code
I now write the types first, even when there’s no one to review them. Not because I’m “designing the API” — usually I’m just trying to figure out what I’m even building. The types are a sketchpad.
When I struggle to write the type, that’s the signal. It usually means I haven’t decided something. Maybe I haven’t decided whether the function returns the thing or a Promise of the thing, and I haven’t decided because I haven’t decided whether the underlying call is sync or async, and I haven’t decided that because I haven’t decided where the data lives. Each of these decisions is small and easy as long as I notice I’m making it. Without the types I’d just write whatever felt right and discover the problem when something broke.
When this argument falls apart
Strong typing is a conversation only when the types carry meaning. If your codebase is dominated by any, unknown, casts, and “type-only” patterns that exist to make the compiler shut up — you have all of the costs of typing and none of the benefits. The conversation is over before it began. Worse: the team has now been trained to ignore type signatures, which means the few honest ones don’t get read either.
The fix is not “more types.” The fix is honest types. A function that legitimately accepts seven different shapes should have a discriminated union, not any. A library boundary that crosses serialization should have parsing, not casting. A “TODO: fix this type” should be a TODO that gets fixed, not a TODO that lives in the codebase for two years until someone gives up and removes the comment.
The escape hatch
I keep one escape hatch open. When I’m prototyping something I genuinely don’t understand the shape of yet, I let the types be loose for a few hours. The cost of “designing the API correctly” before I know what the API is would be paid in worse design — premature commitment to a structure that turns out to be wrong.
Then I tighten. Once the prototype works, I rewrite the types as if I’ve never seen the implementation, just from the outside-in description of what the thing should be. The implementation almost always changes during this rewrite, and almost always for the better. The types have started a real conversation: “is this what the function does, or is this what you want the function to do?”
That conversation is what I show up for.