TypeScript Strict Mode: Fixing 200 Errors in a Legacy Codebase

I flipped "strict": true in a 40,000-line TypeScript project and watched the error count climb to 217. My first instinct was to revert immediately. Instead, I spent a week fixing them all — and the codebase is dramatically better for it. Here’s what I learned.
What Strict Mode Actually Enables
The strict flag is a shorthand for seven individual compiler options:
strictNullChecks—nullandundefinedaren’t assignable to other typesnoImplicitAny— parameters and variables need explicit typesstrictFunctionTypes— stricter function type checkingstrictBindCallApply— type-checksbind,call,applystrictPropertyInitialization— class properties must be initializednoImplicitThis—thismust have a typealwaysStrict— emits"use strict"
The two that generate 90% of the errors? strictNullChecks and noImplicitAny.
Error Category 1: Implicit Any (78 errors)
The biggest category. Function parameters without types, destructured objects without annotations, event handlers with untyped events.
// Before: implicit any everywhere
function processItems(items) {
return items.map(item => item.name.toUpperCase());
}
// After: explicit types
interface Item {
name: string;
quantity: number;
}
function processItems(items: Item[]): string[] {
return items.map(item => item.name.toUpperCase());
}
The fix is straightforward but tedious. My strategy: start with utility functions that many components depend on. Once those have types, type inference handles a lot of the downstream code automatically.
Error Category 2: Null/Undefined Access (92 errors)
This is where the real bugs lived. Code that assumed values would always exist — and sometimes they didn’t. Every strictNullChecks error is a potential runtime crash you’ve been ignoring.
// Before: assumes user always exists
function getUserName(userId: string): string {
const user = users.find(u => u.id === userId);
return user.name; // Error: 'user' is possibly undefined
}
// After: handles the undefined case
function getUserName(userId: string): string | undefined {
const user = users.find(u => u.id === userId);
return user?.name;
}
Some of these revealed actual bugs. We had a function that accessed response.data.items without checking if data was null — it worked 99% of the time, but on API timeouts it threw a cryptic error that we’d been chasing for months.
Error Category 3: DOM and Event Types (31 errors)
Event handlers were a common offender. React events need proper typing:
// Before: untyped event
const handleChange = (e) => {
setQuery(e.target.value);
};
// After: proper React event type
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
};
DOM element access with document.querySelector also needs null guards:
// Before
const el = document.querySelector(".modal");
el.classList.add("active"); // Error: 'el' is possibly null
// After
const el = document.querySelector(".modal");
if (el) {
el.classList.add("active");
}
Strategy: Incremental Adoption
If 200+ errors feels overwhelming, you don’t have to enable full strict mode at once. Enable individual flags one at a time:
{
"compilerOptions": {
"strict": false,
"noImplicitAny": true, // Start here
"strictNullChecks": false, // Enable next
"strictFunctionTypes": false // Then this
}
}
Another approach: use // @ts-expect-error comments temporarily to acknowledge known issues while keeping the build passing. I prefer this over // @ts-ignore because @ts-expect-error will actually error if the underlying issue gets fixed — so your suppressions don’t become permanent.
Patterns That Saved Time
A few patterns appeared repeatedly and had batch-applicable fixes:
Non-null assertion for DOM refs:
// When you know a ref will be populated after mount
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const ctx = canvasRef.current!.getContext("2d");
// The ! is justified because useEffect runs after render
}, []);
Type guard functions for narrowing:
function isValidResponse(data: unknown): data is ApiResponse {
return (
typeof data === "object" &&
data !== null &&
"status" in data &&
"items" in data
);
}
Default values instead of null checks:
// Instead of checking for undefined everywhere
const items = response?.data?.items ?? [];
const count = response?.data?.total ?? 0;
Was It Worth It?
Absolutely. In the first month after enabling strict mode, our production error rate for “cannot read property of undefined” dropped by about 60%. The type system caught several real bugs during the migration — issues that had been silently failing or causing intermittent errors. The code is more verbose in places, but the confidence you get from the compiler actually knowing what’s nullable is worth every extra type annotation.
Written by
Adrian Saycon
A developer with a passion for emerging technologies, Adrian Saycon focuses on transforming the latest tech trends into great, functional products.


