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:

tsconfig.json
{
  "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:

lib/request-state.ts
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
  • readonly on 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.