TypeScript Strict Mode: What It Actually Catches (and What It Doesn't)
A concrete look at what enabling strict mode changes, the bugs it genuinely prevents, and the discipline it doesn't give you for free.
By NavTech Solution
"strict": true is one line in tsconfig.json, and teams frequently treat flipping it on as the whole job. It's a meaningful step, but it's worth being precise about what it actually enables and where its protection stops.
Strict mode is seven flags, not one
strict: true is shorthand for enabling a set of checks together: strictNullChecks, noImplicitAny, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, and alwaysStrict. strictNullChecks is the one that changes the most code, because it means null and undefined are no longer silently assignable to every type.
function getUser(id: string): User | undefined {
return users.find((u) => u.id === id);
}
const user = getUser("123");
console.log(user.name); // error: 'user' is possibly 'undefined'
Without strictNullChecks, that last line compiles fine and throws at runtime the first time getUser returns nothing. With it, the compiler forces you to handle the missing case before you can access .name. This single flag catches an enormous share of real production null-reference bugs before the code ever runs.
What it catches
- Missing null/undefined handling — the example above, at scale, across an entire codebase.
- Implicit
anyleaking types away —noImplicitAnyforces you to type function parameters explicitly, so a typo in a property name gets caught instead of silently becomingany. - Incompatible function signatures passed where a specific callback shape is expected —
strictFunctionTypescatches assigning a function that takes fewer guarantees than the caller expects. - Class fields used before initialization —
strictPropertyInitializationflags a class property declared but never assigned in the constructor.
These aren't edge cases. In most JavaScript-to-TypeScript migrations we run, the first strict-mode pass surfaces real bugs that were already in production, just not yet triggered.
What it doesn't catch
Strict mode checks the types you've declared — it doesn't independently verify that those types are correct. A few gaps worth knowing:
anyyou write on purpose.strictdoesn't stop you from typing somethinganyexplicitly, and oneanyin the wrong place can silently disable checking on everything downstream of it.- Data crossing a runtime boundary. An API response, a form submission, a database row — TypeScript trusts whatever type you tell it that data has. If your API changes shape and your type doesn't, TypeScript won't notice; you need runtime validation (Zod, in most of our projects) at every boundary where data enters your system from outside.
- Logic errors. TypeScript verifies shapes, not intent. A function that adds two numbers when it should subtract them type-checks perfectly.
const UserSchema = z.object({ id: z.string(), name: z.string() });
type User = z.infer<typeof UserSchema>;
// Validates the shape at the actual runtime boundary —
// TypeScript alone can't do this for you.
const user = UserSchema.parse(await response.json());
This is the piece migrations most often skip: teams enable strict mode, fix the compiler errors, and stop — leaving every API call and form submission unvalidated at runtime, which is exactly where the highest-value bugs tend to live.
Migrating an existing JavaScript codebase
Enabling strict: true on a large existing codebase all at once usually produces hundreds of errors and stalls the migration. The more sustainable path:
- Convert files to
.ts/.tsxfirst with strict mode off, fixing only what breaks the build. - Enable strict-mode flags one at a time, starting with
noImplicitAny, thenstrictNullCheckslast (it's almost always the biggest diff). - Convert dependency-ordered — types at the bottom of the dependency graph (API clients, shared utilities) first, so everything built on top inherits real types instead of
any.
Run this alongside normal feature work, not as a separate initiative competing for the same sprint capacity. A migration that never finishes because it was scoped as "stop everything for six weeks" is worse than one that finishes gradually over three months.
If you're planning a migration or setting up strict mode on a new project, this is core to how we approach every TypeScript development engagement — and if the runtime-boundary validation piece above is new to you, it pairs directly with the schema-validation approach we use in AI feature development, where model outputs are exactly the kind of untyped, external data that needs checking at the boundary.