scalable systems handle growth without breaking
← back · 2 min read ·

typescript types as documentation

branded types are a comment that survives the next refactor.

a comment that says // must be in cents, not dollars is a comment. the next person to touch the function will pass dollars anyway, because they didn’t read the comment. they read the signature.

the pattern: branded types

type Cents = number & { readonly __brand: "Cents" };

function charge(amount: Cents) { /* ... */ }

now the signature carries the constraint. a raw number won’t compile. you need a constructor:

const cents = (n: number): Cents => n as Cents;
charge(cents(1999));

that’s the whole pattern. it’s not new — flow had it, fp-ts has it, scala has phantom types, and scott wlaschin’s designing with types is the canonical write-up of the same shape in f#. the move that matters is recognizing that branded types are documentation that compiles.

where to use them

the cost: every branded type needs a constructor (or a guard) at the boundary. that’s where you do the validation. inside the system, you don’t repeat it.

the cost i didn’t expect: junior engineers find the syntax noisy. fair. so i name the brand the same as the type — type Cents = number & { readonly __brand: "Cents" } — and live with the boilerplate. it’s worth it.

a comment that says “must be in cents” is a wish. a Cents type is a contract. one of those rots. the other doesn’t.

scalable labs·cvr 30091604·github·linkedin·hello@scalable.dk