TypeScript at scale: the rules we wish every codebase followed.
TypeScript can make a large codebase safer, easier to refactor and easier to understand. It can also produce a 14-line generic type nobody wants to touch. The goal is useful constraints, not type-system performance art.
TypeScript is a communication tool
The best TypeScript does more than satisfy the compiler. It tells the next developer what data means, which states are valid and what a function promises to return.
In a large codebase, that clarity matters more than cleverness. A type that needs a conference talk to explain is probably not helping the person debugging checkout at 11:30 p.m.
Turn on strictness early
Strict compiler settings — the ones that force you to handle empty values and untyped data — are easiest to switch on at the start. Adding them later to a large, loose codebase becomes a migration project of its own.
Strictness is not there to slow the team down. It pulls vague code into the open, where someone can see it and fix it.
Use unknown at untrusted boundaries
Data from HTTP calls, webhooks, queues, environment variables and third-party SDKs is not trustworthy just because you wrote an interface for it. Mark anything from outside as unknown — TypeScript’s way of saying “we have not established the shape of this yet”. Then check it while the code is running, before you trust it.
Types vanish the moment the code runs. Your payment provider does not care that your editor believed amount was a number.
Model valid states instead of adding booleans
A pile of true/false flags creates impossible combinations: isLoading=true, hasError=true, isSuccess=true. A discriminated union — one type that lists the allowed states, each carrying its own label — describes only the states the product can really be in. The impossible combinations become much harder to write by accident.
It pays off most in workflows, screens that wait on data, payment states and background jobs.
Do not export database shapes everywhere
Database records are storage decisions. API payloads are product contracts. Domain models carry business meaning. The three often overlap, but treating them as one type ties together layers that should be free to change on their own.
Convert between them on purpose, at the boundaries that matter. A little repetition is cheaper than turning a column that is allowed to be empty into a promise your public API has to keep.
Types disappear at runtime. Your payment provider did not read your interface.
Avoid the two extremes: any and wizardry
At one extreme, any switches the checks off and quietly removes the guarantees the team thought it had. At the other, people build types so general that simple code becomes hard to read.
Prefer boring, explicit types. Use generics — types with a placeholder filled in later — only when they remove real duplication. If the type is harder to understand than the function, reconsider.
Keep types close to ownership
Global ‘types.ts’ files tend to become junk drawers. Keep domain types with the domain and component props with the component. Move a type into a shared package only when several parts of the system genuinely share ownership of it.
A type’s location should answer: who is allowed to change this?
TypeScript 6.0 is a reminder to keep configuration current
TypeScript 6.0, released in March 2026, changed several defaults and deprecated a set of old compiler options to clear the path for TypeScript 7.0. The lesson is bigger than one release. Compiler settings are part of the codebase, so review them as deliberately as you review the code.
Do not let tsconfig become an archaeological site full of options nobody remembers choosing.
The rule behind all the rules
Use TypeScript to make important mistakes harder and important ideas clearer. If a rule does neither, it may be ceremony rather than safety.
Found this useful? Build with us.
Tell us what you have in mind. Within 48 hours you'll hear back with an honest plan, clear pricing, and friendly, straight answers.
Start a projectStart a project