tech, developers, and the code underneath

issue 122· essay·

Type systems and the cost of being right

Every type system decision is a trade between what you can prove and what you can express. Here's how to think about where to sit.

Arguments about static versus dynamic typing are mostly tribal at this point, which is a shame, because the actual engineering question is interesting and has a real answer that depends on your situation.

what a type system buys#

Errors found before running. The obvious one. A type error is a bug that cannot reach production, and the earlier a bug is found the cheaper it is.

Documentation that cannot rot. A signature is documentation the compiler checks. Comments lie; types cannot.

Refactoring confidence. Rename a field, and the compiler enumerates every place that breaks. In a dynamic language you grep and hope.

Editor intelligence. Completion, go-to-definition, and find-references are functions of type information. The difference in tooling quality between typed and untyped codebases is enormous and underweighted in these arguments.

Machine-checked verification. The one that got more valuable recently. If generated code has to pass a typechecker, that is verification you did not have to do by reading.

what it costs#

Expressiveness constraints. Some correct programs are rejected. Every type system rejects programs that would have worked, and the more powerful the system, the more elaborate the workarounds for the cases it cannot express.

Ceremony. Type annotations, generic parameters, explicit conversions, interface declarations. Real typing overhead on the keyboard and real reading overhead on the page.

Compile time. Typechecking is work. In a large codebase with a sophisticated type system, it can be a lot of work.

A learning curve that is genuinely steep at the top end. Higher-kinded types, variance, existentials, effect systems. These are powerful and they are also the reason some codebases are unapproachable.

the honest heuristic#

Script under 200 lines that you will run once: dynamic. The type system's benefit does not have time to accrue.

Anything with more than one author: static. The documentation and refactoring benefits scale directly with the number of people who need to understand code they did not write.

Anything that will live more than a year: static. You are the other author, in eight months, with no memory.

Library or API consumed by others: static, and be maximally explicit. Your types are the contract.

Exploratory data work: dynamic, usually. The shapes change constantly and the consequence of an error is a wrong number in a notebook, which you will see.

Anything where a bug is expensive: the strongest type system you can afford, and consider a language with real algebraic data types so you can make illegal states unrepresentable.

the gradual typing reality#

Most of the interesting action is in gradual systems — TypeScript, Python's type hints, Ruby's type tooling, PHP's declarations.

These are genuinely useful and they have a specific failure mode worth naming: the guarantees are only as strong as the least-typed part of the path.

A TypeScript codebase with any scattered through it, or with hand-written type declarations for an untyped dependency that are subtly wrong, provides confidence that is not backed by anything. That is worse than no types, because you trust it.

The practical discipline:

  • Turn on strict mode. strict: true in TypeScript, strict in mypy. The non-strict modes permit exactly the holes that make the system unreliable.
  • Ban any at the boundary. Runtime validation at every point where external data enters — API responses, user input, environment variables, database rows. A parsed-and-validated type is a guarantee; a cast is a wish.
ts
// this is a lie
const user = await res.json() as User;

// this is a guarantee
const user = UserSchema.parse(await res.json());
  • Type the boundaries first. Interior code benefits less. External interfaces benefit most.

the thing that changed#

Type systems got more valuable in the last two years for a reason that has nothing to do with the traditional arguments.

If a substantial share of your code is generated, then every constraint the compiler can check is verification you do not have to perform by reading. And reading is now the bottleneck.

A generated function that typechecks against a well-specified interface has already been verified against one important class of error. A generated function in an untyped language has been verified against nothing.

That is a genuinely new argument and it has moved teams that were unmoved by twenty years of the old ones.

get README in your inbox

One dispatch, no noise. Tech and developer news, plus the occasional long piece on the craft.

subscribe →