typescript

TypeScript Strict Mode: Why It Matters (and How to Migrate)

7 min
typescriptnext.jsweb development

TypeScript Strict Mode: Why It Matters (and How to Migrate)

March 31, 20267 min read
TypeScript Strict Mode: Why It Matters (and How to Migrate)

TypeScript without strict mode catches only half the bugs. Learn what strict mode enables, why it matters, and how to migrate an existing project step by step.

The Bug That Wasn't Supposed to Exist

In early 2025, one of our clients called us with an urgent issue: their checkout flow was occasionally passing undefined to their payment processor, causing random transaction failures that appeared in Stripe logs but generated no error in their application. The bug had existed in production for three weeks before anyone noticed.

When we traced the root cause, it was a classic TypeScript loose-mode failure: a function typed to return User | undefined was being passed directly to another function expecting a concrete User object — and TypeScript, in its default configuration, had said nothing. In strict mode, this would have been a compile-time error. Instead, it was a production incident.

That experience is why every new project we start at LogicLeap uses TypeScript strict mode from day one.

What Is TypeScript Strict Mode?

TypeScript has a strict compiler flag in tsconfig.json. When set to true, it enables a collection of additional type checks that are individually useful but which, together, make your code dramatically safer:

  • **strictNullChecks** — null and undefined are no longer assignable to every type. You must explicitly handle the case where a value might not exist.
  • **strictFunctionTypes** — Function parameter types are checked contravariantly, catching a class of subtle callback-related bugs.
  • **strictBindCallApply** — The bind, call, and apply methods on functions are properly typed.
  • **strictPropertyInitialization** — Class properties must be initialised in the constructor or explicitly marked as possibly undefined.
  • **noImplicitAny** — TypeScript won't quietly fall back to any when it can't infer a type. You have to be explicit.
  • **noImplicitThis** — this expressions with an implied any type raise an error.
  • **alwaysStrict** — JavaScript's 'use strict' is emitted in all output files.

The most impactful of these, by far, is strictNullChecks. Most real-world TypeScript bugs involve values that are unexpectedly null or undefined — this setting makes the compiler catch them before they reach your users.

What Your tsconfig.json Should Look Like

json
{
  "compilerOptions": {
    "strict": true,
    "target": "es2022",
    "lib": ["dom", "dom.iterable", "esnext"],
    "module": "esnext",
    "moduleResolution": "bundler",
    "jsx": "preserve",
    "incremental": true,
    "plugins": [{ "name": "next" }],
    "paths": { "@/*": ["./src/*"] }
  }
}

Setting "strict": true is the single most important change. Everything else is project-specific.

Why Isn't Strict Mode the Default?

You might reasonably ask: if strict mode is so valuable, why isn't it on by default? The answer is backwards compatibility.

TypeScript was designed to be adoptable in incremental steps. If strict mode were the default, migrating a large JavaScript codebase to TypeScript would require fixing hundreds — sometimes thousands — of type errors simultaneously, which is often not feasible in a busy engineering team.

So the TypeScript team made a pragmatic choice: keep the defaults permissive to encourage adoption, but provide an opt-in for teams who want genuine safety. That opt-in is strict: true.

The problem is that many teams never turn it on. They add TypeScript to their project, get some IDE autocompletion benefits, and assume they have type safety. They don't. Without strict mode, TypeScript is significantly less effective — roughly half as good at catching real bugs, in our experience.

The Practical Impact: What Strict Mode Catches

Here's a realistic example of the kind of bug strict mode prevents.

Without strict mode:

typescript
interface User {
  id: number;
  email: string;
  subscription?: {
    plan: string;
    expiresAt: Date;
  };
}

function getSubscriptionPlan(user: User): string {
  return user.subscription.plan; // No error in loose mode
}

In loose TypeScript, this compiles cleanly. At runtime, any user without a subscription causes a TypeError: Cannot read properties of undefined (reading 'plan').

With strict mode:

typescript
function getSubscriptionPlan(user: User): string {
  return user.subscription.plan;
  // Error: Object is possibly 'undefined'
}

TypeScript with strict mode surfaces this immediately. You're forced to handle the undefined case:

typescript
function getSubscriptionPlan(user: User): string {
  return user.subscription?.plan ?? 'free';
}

This is better code. The compiler didn't just catch a bug — it made you write more robust logic.

The noImplicitAny Effect

One of the most valuable aspects of strict mode for team-based development is noImplicitAny. Without it, TypeScript silently assigns any to parameters and variables it can't infer — effectively turning off type checking for that value and everything downstream.

typescript
// Compiles fine without noImplicitAny
function processItems(items) {  // items: any — TypeScript gives up entirely
  return items.map(item => item.name.toUpperCase()); // No checking whatsoever
}

With noImplicitAny, the parameter must be typed explicitly. This forces the author to think about what the function actually accepts — and that decision is documented in code, not left to guesswork.

How to Migrate an Existing Project

If you have an existing Next.js or TypeScript project without strict mode, migrating isn't as painful as it looks — especially if you take it in stages.

Step 1: Enable strict mode and assess the damage

Add "strict": true to your tsconfig.json, then run:

bash
npx tsc --noEmit

This shows you the full scope of type errors without emitting any files. On a mature project, you might see 200–500 errors. Don't panic. Many are the same pattern repeated across many files, and fixing the underlying type definition often resolves dozens at once.

Step 2: Fix strictNullChecks errors first

The majority of your errors will be null-safety issues. Work through them systematically:

  • Add | null | undefined to type definitions where appropriate
  • Use optional chaining (?.) and nullish coalescing (??) to handle undefined values safely
  • Use type guards (if (user !== null)) where the logic requires it
  • Use the non-null assertion operator (!) sparingly — it's a temporary workaround, not a solution

Step 3: Address noImplicitAny warnings

Parameters and variables with inferred any types need explicit annotations. The common cases:

  • Event handler parameters: (e: React.ChangeEvent<HTMLInputElement>) => void
  • Array callbacks where TypeScript can't infer element types
  • Third-party library callbacks with incomplete type definitions

For third-party libraries without types, install the corresponding @types/ package. If none exists, write a minimal .d.ts declaration file in a types/ directory.

Step 4: Use @ts-expect-error for genuinely hard cases

If you have a handful of errors that would require significant refactoring to resolve properly, use @ts-expect-error with an explanatory comment:

typescript
// @ts-expect-error — legacy API returns untyped shape, refactor tracked in #412
const result = legacyApiCall(params);

This is better than @ts-ignore because TypeScript will alert you when the error is resolved and the suppression is no longer needed.

Step 5: Enforce it permanently in CI

Once you've migrated, prevent regression by adding a type check step to your CI pipeline:

yaml
# .github/workflows/ci.yml
- name: Type check
  run: npx tsc --noEmit

If a pull request introduces new type errors, the build fails. This keeps strict mode genuinely strict over time, rather than slowly eroding as the team grows.

Does Strict Mode Slow You Down?

A common objection is that strict mode adds overhead — more type annotations, more compiler errors to fix before you can ship. In our experience, the opposite is true for anything beyond a prototype.

The cost is front-loaded. You spend a few extra minutes writing correct types when authoring code. The payoff is that you spend far less time debugging production issues that TypeScript strict mode would have caught. We estimate a 60–70% reduction in a specific category of runtime bug — null-safety failures, undefined access errors, type mismatch issues — on projects where we've introduced strict mode mid-lifecycle and compared incident rates before and after.

For Next.js projects specifically, strict mode also improves the quality of your server/client boundary typing, which matters increasingly as the App Router encourages more complex data flows between server components and client components. When you're passing data across that boundary, you want the compiler on your side.

Key Takeaways: TypeScript Strict Mode Checklist

Before your next project, or in your next refactoring sprint:

  • Set "strict": true in tsconfig.json on every new TypeScript project
  • Run npx tsc --noEmit to audit existing projects for hidden type errors
  • Fix strictNullChecks violations first — they account for the majority of real-world bugs caught
  • Use ?. and ?? operators rather than ! non-null assertions wherever possible
  • Add tsc --noEmit to your CI pipeline to enforce type safety on every pull request
  • Never add @ts-ignore without a comment; prefer @ts-expect-error with a ticket reference
  • Install @types/ packages for all third-party dependencies; write minimal .d.ts files where none exist

We Build with Strict Mode By Default

At LogicLeap, strict mode isn't optional — it's part of our project scaffold. Every Next.js project we deliver comes with "strict": true baked in from the first commit, alongside ESLint rules that complement TypeScript's static analysis.

When we audited a client's codebase before taking over maintenance, we found 340 strict-mode type errors hiding in code that had been running in production for two years. We resolved them in a structured two-sprint migration, and in the following six months the client reported zero type-related production incidents — down from a running average of roughly two per month.

That's the real value of TypeScript strict mode: it's not an academic exercise in correctness, it's a direct reduction in production incidents and engineering time spent on avoidable debugging. If you'd like your next project built on a properly typed, robust codebase — or if you want us to audit and improve an existing one — get in touch and we'll show you how we work.

Need help implementing this?

We build high-performance websites and automate workflows for ambitious brands. Let's talk about how we can help your business grow.