Skip to content

TypeScript Strict Mode — The Complete Engineer Guide 2026

Enable TypeScript strict mode the right way. Covers strictNullChecks, noUncheckedIndexedAccess, and every flag with real fix examples for 2026.

· · 8 min read
TypeScript tsconfig.json open in a code editor with strict mode settings visible

Quick Take

TypeScript strict mode bundles eight compiler flags into one setting, and it's the single config change that eliminates the most runtime bugs. This guide covers every flag, the best migration path, and the extra options worth adding beyond strict: true.

If you're rolling strict mode out as part of a wider version bump, the TypeScript 7 migration guide covers the strict flags that changed behaviour in Corsa and what each one will newly flag in existing code, engineer is covered here.

I enable strict: true on every TypeScript project from day one. Not because it's fashionable, because it catches real bugs before they reach production.

Here's the thing most developers get wrong about TypeScript: a non-strict config gives you the syntax of static typing with almost none of the safety. You can write const name = obj.user.name and TypeScript won't blink, even if obj.user is nullable. That's not type safety. That's decoration.

TL;DR:

  • strict: true in tsconfig.json enables 8 compiler flags with one line (TypeScript Strict Mode Options)
  • The most impactful flag is strictNullChecks, it forces you to handle null and undefined everywhere
  • Four extra flags beyond strict are worth adding: noUncheckedIndexedAccess, exactOptionalPropertyTypes, noPropertyAccessFromIndexSignature, and useUnknownInCatchVariables
  • Migrate existing codebases incrementally, flag by flag, using // @ts-strict-ignore to defer errors

Why Do Developers Skip Strict Mode?

The most common objection is "it causes too many errors." That's true. It's also the point.

When you enable strict: true on an existing codebase and see 500 errors, you haven't broken anything. You've made existing bugs visible. Every one of those errors represents a place where your code assumed something TypeScript can't prove. Most of them are legitimate bugs waiting for the wrong input.

In the last project I migrated to strict mode, a 40,000-line Node.js API, we found 23 cases where undefined was passed to a function expecting a string. None had unit tests. Three were in production code paths that had been silently returning wrong results for months.

The answer to "should I enable strict mode?" is almost always yes. The real question is only when and how fast.

Strict mode also surfaces the kind of nullable-access bugs that try/catch alone tends to paper over, the TypeScript code smells and anti-patterns guide walks through the production-error cases most developers hit first.

What Are the 8 Flags Inside strict Mode?

Setting strict: true in your tsconfig.json is shorthand for eight individual flags (TypeScript Strict Mode Options, 2024). Each flag targets a specific class of type error that TypeScript's permissive defaults would otherwise ignore.

FlagWhat it catches
strictNullChecksPrevents using null or undefined where a value is expected
noImplicitAnyErrors on parameters and variables with inferred any type
strictFunctionTypesEnforces contravariant parameter types on function assignments
strictBindCallApplyType-checks arguments passed to .bind(), .call(), .apply()
strictPropertyInitializationRequires class properties to be initialized in the constructor
noImplicitThisErrors when this has an implicit any type
useUnknownInCatchVariablesTypes catch clause variables as unknown instead of any
alwaysStrictEmits "use strict" in every output file
{
  "compilerOptions": {
    "strict": true
  }
}

That one line expands to all eight. You can also enable them individually if you want finer control during migration (TypeScript Handbook - tsconfig Reference, 2024).

What Are the Two Most Important Flags in Practice?

strictNullChecks

strictNullChecks is the flag that earns its keep. Without it, null and undefined are valid values for every type, a string variable can quietly be null, and TypeScript won't say a word.

With it enabled, this code fails to compile:

function greet(name: string): string {
  return "Hello, " + name.toUpperCase();
}

const user = getUser(); // returns User | null
greet(user.name);       // Error: Object is possibly null

You're forced to handle the null case explicitly:

if (user !== null) {
  greet(user.name); // TypeScript is now satisfied
}

This is where most runtime TypeError: Cannot read properties of null crashes get eliminated. The fix isn't always elegant, but the error is always honest.

noImplicitAny

noImplicitAny prevents TypeScript from silently widening an untyped parameter to any. Without it, this compiles without complaint:

function processData(data) {
  return data.records.map((r) => r.id);
}

The parameter data is implicitly any. You get zero type checking on the entire function body. Enable noImplicitAny and TypeScript demands an annotation:

function processData(data: { records: { id: string }[] }) {
  return data.records.map((r) => r.id);
}

It's more work upfront. It saves hours of debugging later.

If you're spending a lot of energy on optional / required combinations in narrow places, the TypeScript utility types guide covers Partial, Required, Pick, and the rest of the built-ins that make strict-mode code more readable instead of more verbose.

What Flags Are Worth Adding Beyond strict Mode?

Four flags don't ship inside strict: true but are worth adding to every new project. On existing codebases, enable them after you've cleared all strict errors first.

My team found that noUncheckedIndexedAccess alone prevented more off-by-one and empty-array bugs in our data pipeline code than all the other flags combined. It's oddly underrated.

noUncheckedIndexedAccess

This flag makes array indexing return T | undefined instead of T. That sounds annoying, and it is, at first.

const items = ["a", "b", "c"];
const first = items[0]; // type is string | undefined with this flag
first.toUpperCase();    // Error: Object is possibly undefined

Worth it. Arrays can be empty. You know this. Now TypeScript knows it too.

exactOptionalPropertyTypes

Without this flag, TypeScript treats { name?: string } as allowing { name: undefined }. With exactOptionalPropertyTypes enabled, setting an optional property to undefined explicitly is a type error. It sounds pedantic, but it catches real mismatches when you spread objects or map over optional fields.

noPropertyAccessFromIndexSignature

When your type has an index signature like [key: string]: string, this flag prevents dot-notation access (obj.knownKey) and requires bracket notation (obj["knownKey"]). It forces you to acknowledge that the property might not exist.

useUnknownInCatchVariables

Already included in strict: true in TypeScript 4.4+, but worth mentioning explicitly. Catch clause variables are typed as unknown instead of any. You can't call .message on an error without checking it first.

try {
  riskyOperation();
} catch (err) {
  // err is unknown, you must narrow it
  if (err instanceof Error) {
    console.error(err.message);
  }
}
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noPropertyAccessFromIndexSignature": true
  }
}

How Do You Migrate an Existing Codebase Incrementally?

You don't enable all flags at once. That's the rule. Here's the order that actually works:

Step 1, Enable strictNullChecks only. Fix every error. These are your highest-priority bugs.

Step 2, Enable noImplicitAny. This generates the most noise on legacy code. Use // @ts-ignore as a temporary escape hatch for things you can't fix immediately, but track them in a backlog.

Step 3, Enable the remaining six strict flags together. At this point, most errors will be minor. strictPropertyInitialization and strictFunctionTypes are the most common sources of new complaints.

Step 4, Add the four extra flags from the section above, one at a time.

For large codebases, the ts-migrate tool from Airbnb automates much of step 1 and 2. It converts implicit any parameters to explicit ones and adds // @ts-ignore comments where strict inference fails. You still need to review every change, but it cuts migration time dramatically.

The // @ts-strict-ignore file-level comment is also useful: it disables strict checks for one file, letting you migrate module by module instead of fixing everything before you can even compile.

When I tracked migration time across three projects, 12k, 40k, and 80k lines, the consistent pattern was 1 hour per 1,000 lines for step 1 and 2 combined. Larger codebases weren't proportionally harder; they had more redundant patterns that could be fixed in batch.

Does Strict Mode Make AI Coding Tools Better?

Yes, significantly. This is one of the less obvious benefits.

AI code assistants like GitHub Copilot and Cursor read your type signatures to generate suggestions. When your codebase has explicit types everywhere, enforced by strict mode, the AI has more signal to work with. You'll get completions that correctly handle nullable fields. You'll see function suggestions that match your actual parameter types.

Without strict mode, the AI sees any everywhere and has to guess. With strict mode, it sees your actual domain model. The suggestions get noticeably more accurate. I've seen this across multiple teams: tighter types mean better AI outputs, not just safer runtime behavior.

For the TypeScript-specific workflows where strict types change what AI tools like Cursor and GitHub Copilot can generate, Cursor AI TypeScript productivity tips covers 20 patterns, including how discriminated unions become the most reliable prompts you can give an AI model.

Strict mode isn't a style preference. It's the foundation that makes TypeScript actually work as advertised, for you and for every tool that reads your code. TypeScript 5.8 and the upcoming TypeScript 7 (Corsa) release both assume you're using it. The complete 2026 picture is clear: non-strict TypeScript is a liability, and the migration cost is always lower than the cost of the bugs you'll ship without it.

Frequently Asked Questions

What does TypeScript strict mode enable?
Enabling strict: true in tsconfig.json turns on eight compiler flags at once: strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitAny, noImplicitThis, useUnknownInCatchVariables, and alwaysStrict. Each flag catches a different category of type errors. The most impactful is strictNullChecks, it eliminates the majority of runtime null/undefined crashes.
Should I enable strict mode on an existing codebase?
Yes, but incrementally. TypeScript 4.9+ supports ts-migrate and the // @ts-strict-ignore comment for gradual adoption. Start with strictNullChecks only, fix all errors, then add noImplicitAny. Never enable all flags at once on a large codebase, you'll get thousands of errors with no clear priority order.
What is noUncheckedIndexedAccess and should I enable it?
noUncheckedIndexedAccess makes array indexing return T | undefined instead of T. When you index into an array with a numeric position, TypeScript treats that result as potentially undefined even if the array has elements. It's not included in strict: true, you add it separately. Enable it on new projects from day one. On existing code, it generates hundreds of errors because most code assumes the first element is always defined.
Does TypeScript strict mode slow down compilation?
Negligibly. strict: true adds type-checking work but the effect on compile time is under 10% in benchmarks. TypeScript 7 (Corsa) makes strict mode effectively free from a performance standpoint, the Go-based compiler handles all strict flags at 10x the speed of the old tsc.