I have shipped TypeScript in production for years, and the honest truth is that some strictness pays rent and some is pure ceremony. This is the list that pays.
The two flags that matter most
strict: true is table stakes. These two go further and catch the bugs strict misses:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true
}
}noUncheckedIndexedAccess makes array[i] return T | undefined. Annoying for a day, then it catches the off-by-one you would have shipped.
Make illegal states unrepresentable
The single highest-leverage pattern in TypeScript is the discriminated union:
type RequestState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };Compare that to the "bag of optionals" it replaces:
// Every field is a lie waiting to happen
interface BadState<T> {
loading?: boolean;
data?: T;
error?: Error;
}With the union, data cannot exist alongside error. The compiler enforces what your code review used to catch.
If you find yourself writing
if (state.data && !state.error), you needed a discriminated union three refactors ago.
unknown is your friend, any is a lie
| Type | Meaning | Use when |
|---|---|---|
any |
"Stop checking" | Never, honestly |
unknown |
"Check before use" | Parsing JSON, catch blocks, external input |
never |
"This cannot happen" | Exhaustiveness checks |
Exhaustiveness checking turns the compiler into a to-do list. Add a variant to a union, and every unhandled switch in the codebase becomes a compile error:
function label(state: RequestState<unknown>): string {
switch (state.status) {
case "idle": return "Waiting";
case "loading": return "Loading…";
case "success": return "Done";
case "error": return state.error.message;
default: {
const exhaustive: never = state;
return exhaustive;
}
}
}What to skip
- Branded types for every ID — worth it at 50 engineers, ceremony at 2
readonlyon every property — the compiler cost rarely matches the benefit- Type gymnastics that need a comment to explain — if it needs a comment, it needs a simpler type
Strictness is a budget. Spend it where bugs live: external data, state machines, and array access.

