TypeScript Strict Mode — Why Every Production Codebase Should Enable It
What TypeScript strict mode actually enables, why it catches real bugs, and how to incrementally migrate an existing codebase without stopping feature development.
TypeScript without strict mode is not TypeScript — it's JavaScript with optional annotations that the compiler occasionally checks. The default tsconfig.json generated by most frameworks has strict: false, which means you get almost none of TypeScript's actual protection.
We enable strict mode on every project. Here's exactly what it does and why it matters.
What strict: true Actually Enables
strict: true is a shorthand for eight separate flags:
{
"compilerOptions": {
"strict": true
// Equivalent to enabling all of:
// "strictNullChecks": true,
// "noImplicitAny": true,
// "strictFunctionTypes": true,
// "strictBindCallApply": true,
// "strictPropertyInitialization": true,
// "noImplicitThis": true,
// "alwaysStrict": true,
// "useUnknownInCatchVariables": true
}
}
The two that catch the most real bugs: strictNullChecks and noImplicitAny.
strictNullChecks: Eliminating the Billion-Dollar Mistake
Without strictNullChecks, TypeScript treats null and undefined as assignable to every type. This means the compiler never warns you that a value might be null when you dereference it.
// Without strictNullChecks — compiles, crashes at runtime
function getUserName(userId: string): string {
const user = users.find((u) => u.id === userId) // returns User | undefined
return user.name // TypeError: Cannot read property 'name' of undefined
}
// With strictNullChecks — compile error forces you to handle the case
function getUserName(userId: string): string {
const user = users.find((u) => u.id === userId) // User | undefined
return user.name // Error: Object is possibly 'undefined'
// Correct handling:
return user?.name ?? 'Unknown User'
}
This catches an entire category of runtime errors that would otherwise require defensive programming conventions that are unenforced and therefore unreliable.
noImplicitAny: Making Inference Gaps Explicit
Without noImplicitAny, TypeScript infers any when it can't determine a type — function parameters without annotations, catch block variables, third-party callbacks. any is a type-system hole — anything typed any provides no type checking.
// Without noImplicitAny — silently typed as 'any'
function processData(data) {
return data.user.name.toUpperCase() // No error even if data has no .user.name
}
// With noImplicitAny — error: Parameter 'data' implicitly has an 'any' type
function processData(data: ProcessDataInput): string {
return data.user.name.toUpperCase() // Now type-checked
}
useUnknownInCatchVariables: Safer Error Handling
This one is less discussed but catches real bugs. Without it, catch clause variables are typed as any:
// Without useUnknownInCatchVariables
try {
await api.call()
} catch (error) {
console.log(error.message) // error is 'any' — no type checking
}
// With useUnknownInCatchVariables — error is 'unknown'
try {
await api.call()
} catch (error) {
if (error instanceof Error) {
console.log(error.message) // Safe — TypeScript knows it's an Error
} else {
console.log(String(error)) // Handle non-Error throws
}
}
This forces you to acknowledge that thrown values can be anything, not just Error instances.
Additional Flags We Always Add
Beyond strict: true, these compiler options catch real bugs in production code:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
}
}
noUncheckedIndexedAccess: Array indexing returns T | undefined instead of T. array[0] is possibly undefined, which it is.
exactOptionalPropertyTypes: Distinguishes between an optional property being absent and being explicitly set to undefined. These are different states that often require different handling.
noImplicitReturns: All code paths in a function that return a value must explicitly return.
Migrating an Existing Codebase
Enabling strict mode on a large existing codebase produces hundreds of errors. The practical migration approach: enable flags incrementally using TypeScript project references or per-file suppression.
For a large codebase, use the // @ts-strict annotation approach with ts-migrate:
npx ts-migrate@latest migrate . # adds @ts-ignore comments to all existing errors
Then gradually remove @ts-ignore comments file by file. Each removal cleans up a file to strict standards without blocking the rest of the codebase.
For new projects: enable strict: true from day one. The incremental migration cost is why we enforce it from the first commit.
The Return on Investment
Strict mode errors are a tax paid at compile time instead of production. The exchange rate is heavily in your favour — a null reference error in production can cause hours of incident response, data anomalies, and user-visible failures. The compile-time error costs 30 seconds.
For every project we've migrated to strict mode, the number of runtime errors in production declined measurably in the first release after migration. The correlation is consistent enough that we now treat strict mode as a safety requirement, not a style preference.
If you're inheriting a TypeScript codebase without strict mode and want a systematic migration approach, or if you're starting a new project and want the type system set up correctly, our team can help.