Ch. 2 · TypeScript

TypeScript Strict Compiler Flags

Which checks the TypeScript strict flag turns on, the error each one reports, the extra flags strict leaves off, and how to migrate a codebase.

~6 min readintermediateupdated Oct 19, 2026

“Do you use strict mode?” sounds like a yes-or-no question, and in an interview it isn’t. strict is a single option that switches on a family of separate checks, each catching a different kind of bug, and several valuable checks aren’t in the family at all. A good answer names what’s inside, explains what each check catches, and has an opinion about the extras.

One file, nine errors

The fastest way to understand the family is to break each rule once. This file is valid JavaScript and contains nothing unusual:

function total(items) {
  return items.reduce((sum, i) => sum + i.price, 0);
}

function findUser(id: string): { name: string } | undefined {
  return id === 'u1' ? { name: 'Ada' } : undefined;
}
console.log(findUser('u2').name);

class Session {
  userId: string;
  start() { return this.userId.length; }
}

type Handler = (e: MouseEvent | KeyboardEvent) => void;
const onMouse = (e: MouseEvent) => console.log(e.clientX);
const h: Handler = onMouse;

function greet(this: { name: string }, greeting: string) { return `${greeting}, ${this.name}`; }
greet.call({ name: 'Ada' }, 42);

const counter = {
  n: 0,
  inc: function () { return function () { this.n++; }; },
};

try { JSON.parse('{'); } catch (err) { console.log(err.message); }
TypeScript

Compiled with tsc --strict (TypeScript 7.0, lib including dom):

all.ts(1,16): error TS7006: Parameter 'items' implicitly has an 'any' type.
all.ts(2,24): error TS7006: Parameter 'sum' implicitly has an 'any' type.
all.ts(2,29): error TS7006: Parameter 'i' implicitly has an 'any' type.
all.ts(8,13): error TS2532: Object is possibly 'undefined'.
all.ts(11,3): error TS2564: Property 'userId' has no initializer and is not definitely assigned in the constructor.
all.ts(17,7): error TS2322: Type '(e: MouseEvent) => void' is not assignable to type 'Handler'.
  Types of parameters 'e' and 'e' are incompatible.
    Type 'KeyboardEvent | MouseEvent' is not assignable to type 'MouseEvent'.
      Type 'KeyboardEvent' is missing the following properties from type 'MouseEvent': button, buttons, clientX, clientY, and 14 more.
all.ts(20,29): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
all.ts(24,43): error TS2683: 'this' implicitly has type 'any' because it does not have a type annotation.
all.ts(27,52): error TS18046: 'err' is of type 'unknown'.
Text

With --strict false, the same file compiles with no output at all. Here’s which flag produced each error and why it matters.

noImplicitAny (TS7006). Without it, an unannotated parameter is silently any, and i.price on a misspelled field compiles. This is the flag that makes the rest of the type system worth having, because one any spreads: total would return any, and so would everything that uses it.

strictNullChecks (TS2532). Without it, null and undefined are assignable to every type, so findUser('u2').name compiles and throws TypeError: Cannot read properties of undefined at runtime. Of all the checks, this one catches the most production bugs, and it’s the most work to adopt in an old codebase because every possibly-missing value needs a decision.

strictPropertyInitialization (TS2564). A field declared as string but never assigned is undefined at runtime, so start() would throw. The fixes are a constructor assignment, an initialiser, an optional type, or a definite-assignment assertion (userId!: string) when a framework sets it later. That last one is a promise you make to the compiler, so use it sparingly.

strictFunctionTypes (TS2322 on line 17). A handler that only understands MouseEvent is being stored where it might receive a KeyboardEvent, which has no clientX. With the flag, function-typed values are checked contravariantly in their parameters. Note the exception: parameters of methods (declared with method syntax in an interface) are still checked bivariantly for compatibility with existing DOM and library types.

strictBindCallApply (TS2345 on line 20). Without it, call, apply and bind accept any arguments. With it, greet.call(thisArg, 42) is checked against greet’s real signature.

noImplicitThis (TS2683). The inner function () { this.n++ } doesn’t get counter as this; when it’s called detached, this is undefined in strict-mode JavaScript. The compiler refuses to guess. An arrow function would fix both the type error and the bug.

useUnknownInCatchVariables (TS18046). Anything can be thrown in JavaScript, including strings, so err is unknown rather than any. You narrow with if (err instanceof Error) before reading .message.

Two more members don’t show up in this file. strictBuiltinIteratorReturn types the return value of built-in iterators as undefined rather than any. alwaysStrict emits "use strict" in non-module files; in TypeScript 7.0’s help output it now defaults to true on its own.

The default changed

Compiling the same file with no flags and no tsconfig.json under TypeScript 7.0 gave the same nine errors, because strict now defaults to true (the help text for each family member says “true, unless strict is false”). TypeScript 5.x defaulted it to off. Since projects and tools pin different compiler versions, put "strict": true in tsconfig.json explicitly rather than relying on either default.

What strict leaves out

Some checks are deliberately outside the family, usually because they’re noisier or change the meaning of existing code. This file compiles cleanly under --strict:

const ports = [8080, 8443];
const third = ports[2].toFixed(0);

interface Prefs { theme?: 'light' | 'dark' }
const p: Prefs = { theme: undefined };
console.log('theme' in p);

class Base { save(): void {} }
class Draft extends Base { save(): void {} }

function shippingCents(country: string) {
  if (country === 'US') return 500;
  if (country === 'CA') return 900;
}

function label(code: number): string {
  let s = '';
  switch (code) {
    case 200: s = 'ok';
    case 404: s = 'missing'; break;
  }
  return s;
}

const env: Record<string, string> = { NODE_ENV: 'test' };
console.log(env.NODE_ENV);
TypeScript

Add six extra flags and every one of them fires:

extras.ts(2,15): error TS2532: Object is possibly 'undefined'.
extras.ts(5,7): error TS2375: Type '{ theme: undefined; }' is not assignable to type 'Prefs' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
  Types of property 'theme' are incompatible.
    Type 'undefined' is not assignable to type '"dark" | "light"'.
extras.ts(9,28): error TS4114: This member must have an 'override' modifier because it overrides a member in the base class 'Base'.
extras.ts(11,10): error TS7030: Not all code paths return a value.
extras.ts(19,5): error TS7029: Fallthrough case in switch.
extras.ts(26,17): error TS4111: Property 'NODE_ENV' comes from an index signature, so it must be accessed with ['NODE_ENV'].
Text

In order: noUncheckedIndexedAccess makes ports[2] number | undefined, which is true, since the array has two elements. exactOptionalPropertyTypes separates “property absent” from “property present with value undefined”; they differ at runtime ('theme' in p prints true), and APIs that treat them differently, such as a PATCH body where undefined means “clear this field”, benefit. noImplicitOverride requires the override keyword, so renaming Base.save breaks Draft loudly instead of leaving an orphaned method. noImplicitReturns catches the function that returns undefined for every other country. noFallthroughCasesInSwitch flags the missing break that makes code 200 produce 'missing'. noPropertyAccessFromIndexSignature forces bracket syntax for index-signature keys, so dotted access signals a declared property.

A reasonable shortlist for new projects: noUncheckedIndexedAccess, noImplicitOverride and noFallthroughCasesInSwitch have a high bug-to-noise ratio. exactOptionalPropertyTypes is great for libraries and API types but rough with some third-party typings. noPropertyAccessFromIndexSignature is a style preference.

Turning strict on in an existing codebase

Flipping strict to true on a 200,000-line project can produce thousands of errors, and the worst response is a sweep that adds any or ! until it compiles. A plan that works:

  1. Measure per flag. Set "strict": true and then switch members off individually ("strictNullChecks": false, and so on). Turn each back on in a branch and count: npx tsc --noEmit | grep -c 'error TS'. Some flags, like strictBindCallApply, usually cost a handful of fixes; strictNullChecks is typically the big one.
  2. Land the cheap flags first so the codebase is stricter this week, not after a quarter.
  3. For the expensive flag, stop the bleeding. New code shouldn’t add to the pile. Options include a separate tsconfig.strict.json that includes only migrated directories and runs in CI, or project references so migrated packages compile with stricter settings.
  4. Prefer @ts-expect-error over @ts-ignore for temporary suppressions. It fails when the error underneath disappears, so suppressions don’t outlive their reason.
  5. Fix by meaning, not by syntax. Each strictNullChecks error is a question: can this really be missing? Sometimes the answer is a guard, sometimes a better type upstream, and sometimes a real bug.

Practice question

A team enables strict and gets 1,400 errors, about 1,100 of them TS2532 and TS18048. A developer proposes replacing obj.prop with obj!.prop everywhere the compiler complains, so the flag can be merged today. What would you say?

Answer: The non-null assertion removes the error without removing the risk; it tells the compiler “trust me” 1,100 times, and the codebase ends up with the same runtime crashes plus a false sense of safety, because the next reader sees strict mode and assumes values are checked. I’d split it. Land the other strict members now, since they’re presumably the remaining 300 errors and quick. Keep strictNullChecks off in the main config, add a strict config that covers already-clean directories and runs in CI so no new violations appear, and then migrate module by module, starting with the code that handles external data (API clients, parsers), where missing values are most likely to be real. Where an assertion is justified (a value set by a framework lifecycle hook, for instance), keep it, with a comment saying who guarantees the value. Track the remaining count in CI so the trend is visible.

Further reading

More in TypeScript

read ✓TypeScript · mid

TypeScript Enums vs Union Types

What TypeScript enums compile to, why string enums reject plain strings from JSON, and when an as const object beats both options.

~6 min readread →
esc