Blog
Essays from building production apps with TypeFirst at AgileLabs — plus hands-on recipes and the full Learn TypeFirst tutorial series.
Four booleans can represent sixteen states. Your form has five. Where do the other eleven go? Into production, usually.
TypesAn Email should not be a string. How one generic type and a unique symbol eliminate validation bugs for good.
One contract type shared by server and client — with exhaustive, typed error codes. Rename one and both sides fail to compile.
Error handlingExceptions are invisible in your types. Make failure a value and the compiler will make every caller handle it.
DisciplineEvery escape hatch is a place where your types lie. What happens when you turn all four off — permanently, with lint.
FrontendNo useState. No useEffect. A 50-line Elm-style runtime instead. The honest trade-offs, two years and several client projects later.
The patterns that only show up once a system is live: state machines that grow a variant, IDs that collide, confirmations that arrive after the response — and the bugs types don't catch.
A real payment has five states, not two — and isPaid: false on an authorized card is how you charge someone twice. Sum types for money.
Every third-party webhook, queue message, env var and database row passes a decoder before a handler can touch it. The machinery, and its honest costs.
ProductionThe contract pattern at scale on the endpoint every SaaS grows: a tagged-union body of admin commands, and partial success as a payload the UI must render.
ProductionYour customer ID never changes; the billing provider's does — and theirs isn't unique across accounts. Two bugs pre-empted by two opaque types.
ProductionHTTP 200 didn't mean "done" — confirmation arrives later over SSE. So the four-state union gained WaitingConfirm, and a race condition became a switch case.
347 files, zero hooks, zero eslint-disable, a 55-line runtime — plus the honest costs: monster action files, prop drilling, and one quarantined video player.
ProductionThe other half of the ledger: what an adversarial review found in a heavily-typed codebase, and the pattern behind every gap between shape and meaning.
ts-bedrock is not clever. It is a handful of FP principles applied with total consistency — the whole Core is 2,421 lines. This series rebuilds it from scratch, file by file, explaining the design decision behind each one. By the end you can build the same bedrock in any language.
Four principles, no exceptions — literally. The constitution behind every file that follows, and why consistency beats cleverness.
Build · Part 2Step 0 is configuration: strict tsconfig plus lint rules that ban any, as, is and ! mechanically, not culturally.
Not an Option class — a deliberate T | null with helpers, decoders that normalize undefined, and one named escape hatch.
The _t-discriminated union that replaces every throw in domain code — and how a Result travels over the wire.
The unique symbol trick that actually hides a value at compile time — and why unwrap and toJSON are part of the design.
One module recipe repeated everywhere: an opaque type, a typed error union, two constructors and a decoder. Nat and Password, dissected.
Build · Part 7HTTP, database, localStorage, URLs — every wall gets a decoder, so unknown never leaks inside.
T1 core types, T2 rows, T3 contracts, T4 state, T5 actions — the monorepo layout where every dependency points at Core.
Build · Part 9Method, route, decoders and typed error codes in one importable value — the single source of truth both server and client fulfil.
Build · Part 10Template-literal types extract :tokens from the route string, so renaming a URL param is a compile error, not a 3 a.m. page.
Handlers are pure functions from decoded params to Result — Express is quarantined to one file at the edge.
The database is outside the wall too: every row is decoded on the way out and unwrapped on the way in.
Build · Part 13The exact contract value the server fulfilled now drives fetch, and every response lands as typed RemoteData state.
The Elm Architecture on React with zero hooks: actions are state functions, side effects are data, and the whole loop fits on one screen.
Build · Part 15A Field holds the raw input, its parser and a memoized Result — form errors come from the same typed unions as the domain.
Build · Part 16The payoff: a ten-step porting checklist and the idea-to-file map, from Kotlin to Go. The bedrock was never about TypeScript.
Skip multer and multipart entirely: a streaming upload contract, a size-limited Node pipeline, and an opaque filename that can't traverse paths.
RecipeExtend the T3 contract for streaming: a typed send function on the server, decoded events on the client, each one feeding the TEA runtime as an action.
Mutation is the root of bugs you can't reproduce. Rewrite five everyday functions so data never changes under you.
Learn · Part 2Count your states with cardinality, then design types where invalid combinations simply cannot exist.
Learn · Part 3From MaybeStr and MaybeNum to Maybe<T> — write a type once and keep full safety everywhere it's used.
The outside world sends you unknown, not types. Decode every boundary so bad data is rejected at the door.
The capstone: values that are correct by construction. Validate once with a smart constructor, trust the type forever.