Intermediate 5 min · March 05, 2026

Strict Mode Null Crash - strictNullChecks Prevents It

StrictNullChecks disabled caused 'Cannot read properties of null' on checkout.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
Quick Answer
  • Strict mode enables eight compiler flags that catch null dereferences, implicit any, unsafe function types, and uninitialized class properties.
  • The two most impactful flags: strictNullChecks and noImplicitAny — they catch ~90% of real-world type bugs.
  • For greenfield projects, enable strict:true from the first commit. For legacy codebases, enable it globally, suppress with @ts-nocheck, then fix files incrementally.
  • Performance impact: zero runtime cost. Strict mode is a compile-time guard that emits identical JavaScript.
  • Production insight: A missing strictNullChecks flag caused an e-commerce checkout crash — the code compiled fine but crashed when a user profile was null.

Most TypeScript developers enable strict mode on day one and never think about it again — and that's exactly the problem. When something eventually breaks because of an implicit any or an unchecked null, they have no idea why their 'typed' code behaved exactly like untyped JavaScript. Strict mode isn't just a setting you toggle; it's a philosophy about how seriously you take type safety. It's the difference between TypeScript as a linter and TypeScript as a genuine safety net.

TypeScript was designed to be gradually adoptable, which means its default settings are deliberately lenient. You can migrate a giant JavaScript codebase to TypeScript without changing a single line of logic — which sounds great until you realise you've just added TypeScript syntax without TypeScript's most valuable protections. The problems strict mode prevents — null dereferences, unintended any leakage, implicit function return types — are precisely the bugs that take hours to track down in production.

By the end of this article you'll understand exactly what each flag inside strict mode does, why each one was created, and how to use them in a real project. You'll be able to read a tsconfig.json and immediately know how protected that codebase really is — which is a skill that genuinely separates intermediate TypeScript developers from advanced ones.

What 'strict: true' Actually Enables Under the Hood

Setting 'strict: true' in your tsconfig.json is a shorthand. It doesn't flip one switch — it flips eight. Understanding each one individually is critical because when you migrate a legacy project you'll often need to turn them on one at a time, and when an error appears you need to know which rule it's coming from.

The eight flags that 'strict: true' enables are: strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitAny, noImplicitThis, alwaysStrict, and useUnknownInCatchVariables (added in TypeScript 4.4).

Each one was added in response to a real category of bugs that people kept shipping. They're not academic — they're battle scars. The two you'll feel immediately are noImplicitAny and strictNullChecks. Those two alone will surface more latent bugs in a typical codebase than everything else combined.

You can verify which flags are active at any time by running 'tsc --showConfig' in your terminal. It prints the fully resolved config including all inherited defaults, so there's no ambiguity about what's actually running.

strictNullChecks and noImplicitAny: The Two Flags That Change Everything

These two flags deserve their own section because they're responsible for the vast majority of bugs strict mode catches. Without strictNullChecks, null and undefined can be assigned to any variable of any type — which is exactly how JavaScript works, and exactly why JavaScript is so error-prone.

Consider a function that fetches a user from a database. Without strictNullChecks, you can type the return value as 'User' even though the function clearly might return null when no user is found. TypeScript won't complain. Every caller of that function then assumes they have a User object and calls .name or .email on it — until one day a user isn't found and the whole thing crashes at runtime.

noImplicitAny closes a different gap. TypeScript infers types from usage, but when it genuinely can't infer — like an untyped function parameter — it has two choices: error, or silently assign 'any'. Without noImplicitAny it chooses silence. That silence erases type safety from that point forward in your code, like a hole in a net.

These two flags work together. strictNullChecks narrows the universe of values a type can hold. noImplicitAny ensures that universe is actually defined. Both are essential.

strictPropertyInitialization and useUnknownInCatchVariables in Practice

These two flags are less talked about but protect against genuinely nasty bugs.

strictPropertyInitialization ensures that every property declared in a class is actually assigned a value — either in its declaration or in the constructor. Without it, you can declare a property as 'string' but never assign it, and TypeScript won't warn you. At runtime, accessing that property returns undefined — which breaks your string assumption silently.

The fix is always one of three things: assign it inline, assign it in the constructor, or add the definite assignment assertion (!) if you genuinely know it'll be assigned before use (like by a dependency injection framework).

useUnknownInCatchVariables was added in TypeScript 4.4 and solves a real problem: error handling. Before this flag, catch clause variables were typed as 'any'. That meant you could write 'error.message' without TypeScript complaining — even if the thrown value was a string, a number, or some custom object with no .message property. With this flag, caught errors are typed as 'unknown', forcing you to check the type before using any properties. It's annoying for five minutes, then it saves you hours.

strictFunctionTypes and strictBindCallApply: Two Flags Against Subtle Type Holes

These two flags protect against more subtle type errors that often fly under the radar. They're harder to explain than null checks, but they've caught real bugs in real systems.

strictFunctionTypes enforces contravariance on function parameter types. In plain terms: if you have a function that accepts a 'Dog', TypeScript won't let you pass it where a function accepting 'Animal' is expected. Without this flag, you could accidentally do the reverse — assign a more specific function to a broader parameter type — creating a type hole that could slip through.

strictBindCallApply ensures that when you use .bind(), .call(), or .apply(), TypeScript verifies the argument types against the original function signature. Without it, you could pass the wrong number or type of arguments, and the error only appears at runtime.

Both flags are part of the strict bundle and should stay enabled. They prevent category errors that are extremely hard to debug because they only manifest when specific argument types are passed at runtime.

Migrating a Real Project to Strict Mode Without Breaking Everything

The biggest practical question isn't 'what does strict mode do?' — it's 'how do I turn it on in a codebase that didn't start with it?'

The naive approach is to add 'strict: true' and try to fix every error. In a large codebase this can surface hundreds of errors at once, which is demoralising and creates enormous PRs that are hard to review. There's a better strategy.

TypeScript supports per-file override via 'ts-ignore' and the newer '// @ts-nocheck' comment. The migration pattern that actually works in production: enable strict at the project level, immediately suppress all errors in existing files using @ts-nocheck, then remove the suppression comment from each file you touch during normal feature work. The codebase gets safer every sprint without ever blocking progress.

For greenfield projects, there's no excuse — strict: true should be in your tsconfig from the very first commit. The cost of enabling it later grows quadratically with codebase size. CRA, Vite, and Next.js all scaffold with strict mode on by default now, which is the right call.

Two flags worth adding beyond strict: 'noUncheckedIndexedAccess' (array indexing returns T | undefined) and 'exactOptionalPropertyTypes' (truly optional vs. explicitly undefined). Neither is in the strict bundle but both catch real bugs.

TypeScript Strict Mode Flags Compared
FlagWhat It CatchesWithout It, You Risk
strictNullChecksnull/undefined used as non-nullable typesRuntime TypeError on .property access
noImplicitAnyUntyped function params/variables silently typed as 'any'Type safety silently disabled for entire code paths
strictPropertyInitializationClass properties declared but never assignedAccessing a property that's secretly undefined at runtime
useUnknownInCatchVariablesAccessing .message on a caught error without type checkingCrashing in your error handler — the worst place to crash
strictFunctionTypesUnsafe function type assignments that create type holesFunctions receiving the wrong argument types with no error
strictBindCallApply.call()/.bind()/.apply() called with wrong argument typesType-unsafe invocations that only fail at runtime
noUncheckedIndexedAccess (extra)Array/object indexing that might return undefinedTreating a potentially missing value as definitely present
exactOptionalPropertyTypes (extra)Optional properties that should not be explicitly set to undefinedAccidentally setting undefined where the API expected the property to be absent

Key Takeaways

  • 'strict: true' is a shorthand for eight separate flags — knowing each one individually means you can diagnose errors instantly and enable them incrementally during migration.
  • strictNullChecks and noImplicitAny are the two highest-impact flags — between them they prevent the two most common categories of TypeScript bugs: null dereferences and silent any leakage.
  • useUnknownInCatchVariables types caught errors as 'unknown', not 'any' — this forces you to write real error handling instead of assuming every thrown value is a standard Error object.
  • noUncheckedIndexedAccess and exactOptionalPropertyTypes aren't in the strict bundle but are worth adding to every greenfield project — they close real gaps that strict alone leaves open.
  • The @ts-nocheck migration strategy lets you adopt strict mode incrementally without blocking development: enable strict globally, suppress old files, fix them one by one during normal work.

Common Mistakes to Avoid

  • Using '!' (non-null assertion) everywhere to silence strictNullChecks errors
    Symptom: Code compiles but still crashes with TypeError in production. The '!' operator tells TypeScript 'this is definitely not null' — if you're wrong, TypeScript can't help you.
    Fix: Narrow with an actual null check (if statement or optional chaining) instead of asserting your way out of the error.
  • Setting strict: true in tsconfig but having a tsconfig.app.json or tsconfig.build.json that overrides it with strict: false
    Symptom: Errors appear in your IDE but not in CI, or vice versa. Leads to false confidence and shipping bugs.
    Fix: Run 'tsc --showConfig' (not 'tsc --version') to see the actual resolved config your build is using. Always verify the file your build tool is actually reading.
  • Typing caught errors as 'any' manually to work around useUnknownInCatchVariables
    Symptom: Developers write 'catch (error: any)' to get the old behaviour back. This defeats the entire purpose of the flag.
    Fix: Write a type guard function (like 'isApiError' shown above) or use 'instanceof Error' for standard errors. The two-minute effort to write a guard prevents hours of debugging a broken error handler.
  • Ignoring noUncheckedIndexedAccess because 'my arrays are always complete'
    Symptom: Accessing array elements with dynamic indices returns undefined at runtime, crashing your UI or service.
    Fix: Enable noUncheckedIndexedAccess. Then use early returns, optional chaining, or Array methods like .at() that return undefined. Your code will be safer and more explicit.

Interview Questions on This Topic

  • QWhat's the difference between enabling 'strict: true' and manually listing all the strict flags individually in tsconfig? When would you choose one over the other?SeniorReveal
    strict: true is shorthand. It enables all eight flags at once. Manually listing them allows you to exclude specific flags that might be too disruptive during a migration. For example, you could enable noImplicitAny and strictNullChecks but leave strictFunctionTypes off until you've fixed callback-heavy areas. However, for greenfield projects, always use strict: true — it's simpler and ensures you get all protections.
  • QWith strictNullChecks enabled, how would you safely handle a function that might return null or undefined? Walk me through at least two patterns.Mid-levelReveal
    Two patterns: 1) Early return with a guard: const result = getData(); if (result === null) return default;. 2) Optional chaining with fallback: const name = user?.name ?? 'Unknown';. The key is that TypeScript narrows the type within the conditional block. You can also use the nullish coalescing operator ?? to provide a default when the value is null or undefined.
  • QWhy are caught errors typed as 'unknown' rather than 'Error' in strict mode, and what does that tell you about how TypeScript thinks about exception safety?SeniorReveal
    In JavaScript, you can throw anything — strings, numbers, objects, null, even undefined. If TypeScript typed caught errors as Error, you'd have a false sense of safety because the actual thrown value might not have a .message property. By typing them as unknown, TypeScript forces you to write code that actually checks what was thrown before using it. This reflects TypeScript's design philosophy: don't assume — verify. It's the same principle that makes strictNullChecks so powerful: honest types prevent silent assumptions.
  • QWhat is the definite assignment assertion ! and when is it appropriate to use it?Mid-levelReveal
    The ! operator tells TypeScript 'this value is definitely not null/undefined at this point' or 'this property will be assigned before it's read'. Appropriate uses: 1) Class properties set by a dependency injection framework after instantiation. 2) Variables that are assigned inside a callback that runs synchronously (e.g., let result!: string; setTimeout(() => result = 'done', 0); — but be careful with async). 3) When you have a guard upstream that TypeScript can't reason about. Inappropriate uses: as a lazy workaround for real nullable logic. Overusing ! erodes the safety strict mode provides.

Frequently Asked Questions

Does enabling strict mode in TypeScript affect runtime performance?

No. Strict mode is entirely a compile-time feature. It only changes what the TypeScript compiler checks and rejects — the JavaScript it emits is identical whether strict is on or off. All the type information is erased during compilation.

Should I enable strict mode in an existing JavaScript project I'm migrating to TypeScript?

Yes — but do it incrementally. Enable 'strict: true' immediately, then add '// @ts-nocheck' to every file that breaks. Remove the suppression comment file by file as you work through the codebase. This gives you the safety net on new code right away without blocking the migration.

What's the difference between 'unknown' and 'any' in catch clauses?

With 'any', TypeScript lets you access any property without checks — it trusts you completely. With 'unknown', TypeScript refuses to let you access any property until you prove the type via a type guard or instanceof check. 'unknown' is the safe default: it forces you to handle the possibility that the thrown value isn't what you expect.

Can I mix strict and non-strict files in the same project?

Technically no — strict is a project-wide compiler option. However, you can simulate per-file strictness by adding '// @ts-nocheck' at the top of files you haven't fixed yet. Those files are effectively ignored, and new code in other files benefits from full strict checking. This is the recommended migration approach.

Is there any reason NOT to enable strict mode?

Only during an incremental migration of a very large, untyped codebase where fixing all issues at once would block development for weeks. Even then, you should still enable strict at the project level and suppress old files. For new projects, there is no valid reason to disable it.

🔥

That's TypeScript. Mark it forged?

5 min read · try the examples if you haven't

Previous
JavaScript vs TypeScript: Key Differences
11 / 15 · TypeScript
Next
TypeScript Declaration Files