Type safety
A type is a proof the compiler rechecks for free on every build, but only
about the things you let it see. Model with loose types (everything a
string, every field optional, states tracked by scattered flags) and the
compiler cannot help; model tightly and whole categories of bug stop
compiling.
Method
- Make illegal states unrepresentable. Replace a
status: string plus
a nullable error with a tagged union: { kind: 'loading' } | { kind: 'ok', data: T } | { kind: 'error', message: string }. Now "ok with an
error message" cannot be constructed, so no code has to guard against
it.
- Parse, do not validate, at the edge. Turn input into a precise type
once (
Email, UserId, NonEmptyList) with zod, io-ts, or pydantic,
then pass that type inward. A function taking Email never re-checks the
@, because the type already carries the proof.
- Wrap primitives that mean different things. A
UserId and an
OrderId that are both bare string will get swapped eventually.
Branded types, newtypes, or NewType in Python make the swap a compile
error at no runtime cost.
- Keep required fields required. Make a field optional only when
absence is a real, handled case. Every
? you add is a branch every
caller inherits, so keep the mandatory fields mandatory and let the type
document what must exist.
- Turn the checker to strict.
strict: true in tsconfig, mypy
--strict, Kotlin explicit-API mode, the relevant -Werror analyses.
Half-on type checking passes files a full pass would reject, which is
worse than knowing they are unsound.
- Make exhaustiveness a compile error. Switch on a union with a
never default (TypeScript) or a sealed when (Kotlin) so adding a
variant fails the build at every site that does not yet handle it. The
compiler becomes your checklist for the change.
Checks
- Can you construct a value that is in two contradictory states at once? If
so, tighten the type.
- Does adding a new enum case break the build at every site that must
change?
- Are
UserId and OrderId distinguishable to the compiler, or both just
string?
Boundaries
Dynamic languages without gradual typing, and seams with untyped systems
(raw JSON, FFI, reflection), cap how much the compiler can prove: validate
at those seams instead. Type modeling has a cost, and past a point the
ceremony outweighs the bugs prevented, so stop tightening when the next
constraint costs more than the mistake it would block.
1---2name: type-safety3description: Model data so illegal states will not compile, then turn the checker to strict and let it prove correctness for free. Use when designing types, modeling state machines, or hardening a loosely typed module.4---56# Type safety78A type is a proof the compiler rechecks for free on every build, but only9about the things you let it see. Model with loose types (everything a10string, every field optional, states tracked by scattered flags) and the11compiler cannot help; model tightly and whole categories of bug stop12compiling.1314## Method15161. **Make illegal states unrepresentable.** Replace a `status: string` plus17 a nullable `error` with a tagged union: `{ kind: 'loading' } | { kind:18 'ok', data: T } | { kind: 'error', message: string }`. Now "ok with an19 error message" cannot be constructed, so no code has to guard against20 it.212. **Parse, do not validate, at the edge.** Turn input into a precise type22 once (`Email`, `UserId`, `NonEmptyList`) with zod, io-ts, or pydantic,23 then pass that type inward. A function taking `Email` never re-checks the24 `@`, because the type already carries the proof.253. **Wrap primitives that mean different things.** A `UserId` and an26 `OrderId` that are both bare `string` will get swapped eventually.27 Branded types, newtypes, or `NewType` in Python make the swap a compile28 error at no runtime cost.294. **Keep required fields required.** Make a field optional only when30 absence is a real, handled case. Every `?` you add is a branch every31 caller inherits, so keep the mandatory fields mandatory and let the type32 document what must exist.335. **Turn the checker to strict.** `strict: true` in tsconfig, mypy34 `--strict`, Kotlin explicit-API mode, the relevant `-Werror` analyses.35 Half-on type checking passes files a full pass would reject, which is36 worse than knowing they are unsound.376. **Make exhaustiveness a compile error.** Switch on a union with a38 `never` default (TypeScript) or a sealed `when` (Kotlin) so adding a39 variant fails the build at every site that does not yet handle it. The40 compiler becomes your checklist for the change.4142## Checks4344- Can you construct a value that is in two contradictory states at once? If45 so, tighten the type.46- Does adding a new enum case break the build at every site that must47 change?48- Are `UserId` and `OrderId` distinguishable to the compiler, or both just49 `string`?5051## Boundaries5253Dynamic languages without gradual typing, and seams with untyped systems54(raw JSON, FFI, reflection), cap how much the compiler can prove: validate55at those seams instead. Type modeling has a cost, and past a point the56ceremony outweighs the bugs prevented, so stop tightening when the next57constraint costs more than the mistake it would block.