Turning on strict: true can feel rude. Code that worked yesterday suddenly has red lines everywhere.

But TypeScript is not being dramatic. It is telling you where your program relied on assumptions that were never written down.

Quick answer

strict: true makes TypeScript ask better questions:

  • What type is this value?
  • Can it be null or undefined?
  • What does this refer to?
  • Did this class field definitely get initialized?
  • Is the caught error really an Error object?

That feels noisy at first, but the noise usually points at places where runtime bugs were already possible.

strict is a bundle

This setting:

{
  "compilerOptions": {
    "strict": true
  }
}

enables a group of stricter checks. The most visible ones are:

  • noImplicitAny
  • strictNullChecks
  • strictFunctionTypes
  • strictBindCallApply
  • strictPropertyInitialization
  • noImplicitThis
  • useUnknownInCatchVariables

You do not need to memorize every flag immediately. But understanding the big ones makes strict mode much less mysterious.

noImplicitAny

Without strict mode, TypeScript may silently give a value the type any:

function formatUser(user) {
  return user.name.toUpperCase();
}

With noImplicitAny, TypeScript asks you to be explicit:

type User = {
  name: string;
};

function formatUser(user: User) {
  return user.name.toUpperCase();
}

This matters because any turns off type safety. Once any enters your code, it can spread.

strictNullChecks

This is the strict mode option that catches a huge class of real bugs.

Without it, TypeScript is too relaxed about null and undefined. With it, this code fails:

const user = users.find((user) => user.id === id);

return user.name;

Why? Because find can return undefined.

The fix is to handle the missing case:

const user = users.find((user) => user.id === id);

if (!user) {
  throw new Error("User not found");
}

return user.name;

This is not busywork. It forces you to decide what should happen when data is missing.

noImplicitThis

JavaScript’s this can be slippery. TypeScript can catch cases where this does not mean what you think.

const counter = {
  count: 0,
  incrementLater() {
    setTimeout(function () {
      this.count++;
    }, 100);
  }
};

Inside the regular function, this is not the object you probably intended.

Use an arrow function:

const counter = {
  count: 0,
  incrementLater() {
    setTimeout(() => {
      this.count++;
    }, 100);
  }
};

Strict mode helps reveal those context bugs before runtime.

strictPropertyInitialization

Classes become safer too:

class Job {
  title: string;
}

TypeScript complains because title is declared but never initialized.

You can fix it with a constructor:

class Job {
  title: string;

  constructor(title: string) {
    this.title = title;
  }
}

Or make the property optional if that matches reality:

class Job {
  title?: string;
}

The right answer depends on whether every Job must have a title.

useUnknownInCatchVariables

In JavaScript, anything can be thrown:

throw "failed";
throw { message: "failed" };
throw new Error("failed");

Strict mode treats caught errors as unknown, not automatically as Error:

try {
  await runJob();
} catch (error) {
  console.log(error.message);
}

You need to narrow it:

try {
  await runJob();
} catch (error) {
  if (error instanceof Error) {
    console.log(error.message);
  }
}

That feels annoying until you work with a codebase where random values are thrown.

Why strict mode breaks old code

Strict mode usually breaks code for honest reasons:

  • Parameters were never typed.
  • Missing data was assumed to exist.
  • A value could be undefined.
  • this depended on runtime binding.
  • Classes declared fields but did not initialize them.
  • Errors were assumed to be Error instances.

These are not TypeScript problems. They are program assumptions becoming visible.

How to migrate without pain

If a project is large, do not try to fix everything in one night.

Use a staged path:

  1. Turn on noImplicitAny.
  2. Fix public function parameters.
  3. Turn on strictNullChecks.
  4. Fix the highest-risk data paths first.
  5. Avoid replacing every error with as any.

The goal is not to make the compiler quiet. The goal is to make the code more honest.

a safer migration example

Suppose a service reads an optional environment variable and immediately uses it:

const port = Number(process.env.PORT);
server.listen(port);

Strict mode cannot prove that PORT exists or contains a number. Moving that uncertainty to one boundary gives the rest of the application a reliable value:

function readPort(value: string | undefined): number {
  const port = Number(value);

  if (!Number.isInteger(port) || port < 1 || port > 65_535) {
    throw new Error("PORT must be an integer between 1 and 65535");
  }

  return port;
}

const port = readPort(process.env.PORT);
server.listen(port);

This is the useful pattern behind strict TypeScript: accept uncertainty at input boundaries, validate it once, and pass a narrower type into the core of the program. Avoid scattering non-null assertions such as process.env.PORT! through the code. They silence the compiler without making the value safer.

The habit strict mode teaches

Strict TypeScript makes you answer better questions:

  • Can this value be missing?
  • What shape does this object actually have?
  • Who owns this error case?
  • Is this function safe to call with any input?

That is why strict mode is useful. It moves bugs from production into your editor.