---
title: "Types are a conversation, not a contract"
description: "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."
pubDate: 2026-01-19
tags: ["Type Systems", "Engineering Habits"]
---

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:

```ts
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.
