TechnologyJune 20, 20261 min read

Five TypeScript habits that survive large codebases

Types are a communication channel to the next engineer. These habits keep that channel honest when the repo passes 100k lines.

TypeScript in a weekend project and TypeScript in a multi-year platform are different sports. These are the habits that kept paying off as codebases I work on grew.

1. Make illegal states unrepresentable

A request is not { loading: boolean; data?: T; error?: string }. That type allows loading with data, or error with data — states your UI will eventually render by accident. Model it as a union: idle, loading, success with data, failure with error. The compiler now maintains your invariants for free.

2. Parse, don't assert

Every boundary — API response, form input, environment variable — is untyped at runtime no matter what the annotation says. A schema (Zod in my stack) turns "trust me" into "verified". The habit: types describe what you checked, not what you hope.

3. Let inference carry the weight

Explicit annotations belong on exported function signatures and public APIs. Inside a function, inference is usually smarter than you and never goes stale. Annotating everything is not rigor, it is noise that hides the annotations that matter.

4. Unknown over any, always

any turns the type checker off for everything it touches — it spreads. unknown keeps the door locked until you prove the shape. The five minutes of narrowing costs less than the runtime crash three sprints later.

5. Types near the data, not in a types/ dump

A god-file of 400 interfaces is where types go to rot. Co-locate them with the module that owns the data, export the few that form a contract, and delete the rest when the module dies. Types are code; they deserve the same hygiene.

Enjoyed this one?

I write about what I build. Let's talk about yours.

Get in touch