The error was a single line: `Cannot read properties of undefined (reading 'name')`. It appeared in production on a Friday evening, affecting a dashboard used by field researchers across three countries. The fix was trivial — a missing null check on an API response that occasionally returned an empty array instead of the expected object. The type definition said `User`. The runtime said `undefined`.
That incident cost us a sprint. Not because the fix was complex, but because the trust was broken. The field team lost a day of data collection. The product manager questioned our testing process. The retrospective was uncomfortable. And the root cause was not a logic error or a missing test — it was a type system that had been configured to lie.
Our TypeScript configuration at the time was what I now call "decorative typing." Strict mode was off. `any` appeared in forty-three files. Return types were inferred but never verified against API contracts. The compiler was running, but it was not protecting us. It was generating JavaScript and staying out of the way.
The week after the incident, I proposed a migration to strict TypeScript. Not as a refactoring exercise — as a safety measure. The team was skeptical. Strict mode would surface hundreds of errors in existing code. It would slow down feature delivery in the short term. It would require rewriting type definitions that had been copy-pasted from documentation without verification.
I started with `strictNullChecks`. This single flag surfaced over two hundred errors across the codebase. Every one of them was a potential production bug — a property access on a value that could be `null` or `undefined`, unchecked by the developer and invisible to the compiler. We fixed them in batches, over two weeks, alongside regular feature work.
Next came `noImplicitAny`. This was harder. It required going back to every function parameter, every variable declaration, every callback that had been written without type annotations. The process was tedious but revealing. We discovered three API endpoints where the frontend's type expectations did not match the backend's actual responses. These mismatches had been silently causing subtle bugs that users had been working around without reporting.
The most impactful change was introducing discriminated unions for API responses. Instead of typing every response as the happy-path shape and hoping for the best, we modeled the full range of possibilities: loading, success, error, empty. Components that consumed API data were forced to handle every case. The compiler would not let you render a user's name without first confirming that the user existed.
Within three months, the results were clear. Runtime type errors — the category that had caused the original incident — dropped to near zero. Code reviews became faster because the type system caught the mechanical errors, freeing reviewers to focus on logic and design. New developers onboarding to the project could read the types and understand the data flow without tracing through runtime behavior.
The team that had been skeptical became the team that enforced the standard. Pull requests with `any` types were rejected. API contracts were validated with Zod schemas at the boundary. The type system was no longer decorative — it was structural. It carried weight.
The lesson I took from this experience is not that TypeScript is good. That is obvious. The lesson is that TypeScript configured permissively is worse than no TypeScript at all. It creates a false sense of safety. It lets you believe the compiler is watching when it is not. Strict mode is not a preference — it is the minimum viable configuration for a production codebase. Everything below it is theater.
