Enabling TypeScript Strict Mode: The Flag That Earns Its Keep
- typescript
- javascript
- tooling
- best-practices
- backend
There is a version of TypeScript that catches a huge share of your bugs before they run, and a version that mostly just adds annotations while letting the same old JavaScript mistakes through. The difference between them is one line in your tsconfig.json: "strict": true. Without it, TypeScript is politely optional about the checks that matter most. With it, the type system actually earns its keep. If you take one action after reading this series, make it turning strict mode on — and this post is about what that flag does and how to adopt it without a painful weekend of red squiggles.
This is part of the TypeScript type system series. Everything else in the series is more valuable once strict mode is on, because that is what makes the type system enforce rather than suggest.
What "strict" actually turns on
"strict": true is not a single check; it is a switch that enables a family of stricter flags together. A few of them do most of the work.
strictNullChecks is the big one, and on its own it justifies the whole feature. Without it, null and undefined are assignable to every type, so a string might secretly be null and TypeScript says nothing — which means the single most common runtime error in JavaScript, the "cannot read property of undefined," sails right past the type checker. With it on, null and undefined are their own types that you must handle explicitly:
function greet(name: string | null) {
return `Hi, ${name.toUpperCase()}` // error: name might be null
}
The error is the point. The compiler now forces you to narrow away the null case before using the value, which means the class of null-reference crashes becomes something you handle at compile time instead of discovering in production.
noImplicitAny stops TypeScript from silently falling back to any when it cannot infer a type. Without it, an untyped parameter quietly becomes any and all safety on it vanishes with no warning. With it, you get an error telling you to annotate, so any only ever enters your code because you chose it, not because you forgot.
strictFunctionTypes, strictBindCallApply, and strictPropertyInitialization round out the set, tightening how function types are compared and ensuring class properties are actually assigned. They catch subtler issues, but the headline value is in the first two.
Why the loose mode is a trap
It is worth being blunt about this. TypeScript without strict mode gives you a lot of the ceremony of types — the annotations, the interfaces, the tooling — while quietly declining to enforce the one guarantee that prevents the most bugs. A codebase that has types everywhere but runs with strictNullChecks off is carrying the cost of TypeScript without collecting the main benefit. Values you believe are present can be null; parameters you think are typed are secretly any. The type system is there, but it has been told to look the other way at exactly the moments that matter.
New projects should start strict from the first commit. Every modern scaffolding tool sets "strict": true by default now, and there is no good reason to turn it off. The cost of writing strict-mode code from the start is near zero, because you never accumulate the unsafe patterns that strict mode would reject; you just write correct code from the beginning.
Migrating an existing codebase
The hard case is an existing project — a JavaScript codebase, or a TypeScript one that has been running loose — where flipping "strict": true lights up hundreds of errors at once. The mistake is treating that as a single wall to climb. It is better done incrementally, and TypeScript gives you the tools to do it flag by flag.
Rather than enabling strict wholesale, turn on the individual flags one at a time, starting with the highest-value and working through the fallout of each before moving to the next. noImplicitAny first is a common choice: it surfaces everywhere inference has silently given up, and fixing those often clarifies a lot of code. Then strictNullChecks, which is usually the largest batch of errors and the most valuable, so it is worth its own dedicated pass. Each flag you enable and clear is permanent progress that cannot regress, because the compiler now enforces it.
For a large migration, work module by module as well as flag by flag. You can keep the strict flags off globally while tightening the parts of the codebase you actively touch, so new and changed code meets the bar even before the legacy corners do. The enterprise folder structure and monorepo setups pair well with this, since a well-bounded module is a natural unit to make strict in isolation. The direction that matters is monotonic: every file you bring up to strict stays strict, and over time the untyped surface shrinks to nothing.
Resist the escape hatches during migration
When the errors pile up, the temptation is to silence them fast — sprinkle any, add as casts, drop // @ts-ignore comments — and move on. Every one of those is borrowing against the future. An any added to quiet a strictNullChecks error re-opens exactly the hole strict mode was closing, and it will be there long after you have forgotten why. The occasional escape hatch at a genuine boundary is fine, but using them as a bulk migration strategy defeats the entire exercise: you end up with strict mode enabled and a codebase full of holes punched through it.
The honest path is to actually handle the cases the compiler surfaces — check for null, annotate the parameter, model the state properly. That work is not busywork; each error strict mode raises is a real place your code could have crashed, and fixing it is fixing a latent bug. Done that way, turning on strict mode is one of the highest-return things you can do to a codebase, because it converts a category of production incidents into compile errors you clear once and never see again. That is the whole promise of the type system, and strict mode is the switch that makes it real.
Key takeaways
- "strict": true enables a family of flags together; strictNullChecks and noImplicitAny do most of the work.
- strictNullChecks makes null and undefined explicit types you must handle, catching the most common JavaScript runtime error at compile time.
- noImplicitAny stops TypeScript silently falling back to any, so any only enters your code when you choose it.
- Loose mode gives you the ceremony of types without the main guarantee; new projects should start strict from the first commit.
- Migrate an existing codebase incrementally — flag by flag and module by module — and resist bulk any/as/@ts-ignore escape hatches that re-open the holes.
Frequently asked questions
What does TypeScript strict mode do?
Setting "strict": true enables a group of stricter checks at once, most importantly strictNullChecks (null and undefined must be handled explicitly) and noImplicitAny (no silent fallback to any). Together they make the type system enforce the guarantees that prevent the most common runtime bugs.
Why is strictNullChecks so important?
Without it, null and undefined are assignable to every type, so the most common JavaScript crash — reading a property of undefined — passes the type checker silently. With it on, you must narrow away the null case before using a value, turning those crashes into compile-time errors.
How do I migrate an existing codebase to strict mode?
Incrementally, not all at once. Enable individual flags one at a time starting with the highest value (often noImplicitAny, then strictNullChecks), clearing the errors from each before the next. For large codebases, also tighten module by module so new and changed code meets the bar first.
Should I use any or @ts-ignore to speed up a strict migration?
Not as a strategy. Each any, as cast, or @ts-ignore added to silence an error re-opens the exact hole strict mode was closing, and it outlives the memory of why it is there. Handle the cases properly instead — each error strict mode raises is a real place the code could have crashed.
Further reading
Explore more on

About the author
I'm Aman Kumar Singh, a software engineer in Noida, India building scalable full-stack products with React, Next.js, Node.js, NestJS, PostgreSQL, Redis, and AWS. I write about backend engineering, distributed systems, and system design.