DEV Community

Cover image for TypeScript Strict Mode — Why I Use It in Every Project
Safdar Ali
Safdar Ali

Posted on • Originally published at safdarali.in

TypeScript Strict Mode — Why I Use It in Every Project

I'm Safdar Ali, a frontend engineer in Bengaluru. Three years ago I turned off strict mode on a client dashboard because the migration felt noisy. Two weeks later, a user with an empty profile name crashed the settings page in production — exactly the class of bug strictNullChecks would have flagged at compile time. Since then, typescript strict mode is non-negotiable on every React and Next.js repo I touch. This guide is the tsconfig I copy, the flags I care about, and the errors that actually save you hours.

What "strict mode" actually means in TypeScript

Strict mode is not one switch — it is a family of compiler checks bundled under strict: true in tsconfig.json. When enabled, TypeScript refuses code that relies on implicit any, unchecked null access, or loose function types.

The tradeoff is upfront friction: your first week on an old JavaScript codebase will surface hundreds of errors. The payoff is fewer 2am Slack messages and safer refactors when you rename a prop across forty components.

// tsconfig.json — baseline I paste into new Next.js projects
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": { "@/": ["./"] }
},
"include": ["next-env.d.ts", "/*.ts", "/.tsx", ".next/types//.ts"],
"exclude": ["node_modules"]
}

strict: true turns on strictNullChecks, noImplicitAny, and several related flags. You can enable them individually, but I have never found a good reason to run half-strict on a greenfield app.

Strict flags — what each one does

Flag Catches Pain level
strictNullChecks null / undefined used as values High on legacy APIs
noImplicitAny Parameters without types Medium
strictFunctionTypes Unsafe callback assignments Low–medium
strictBindCallApply Wrong bind/call/apply args Low
noImplicitThis Ambiguous this in functions Low
alwaysStrict Emits "use strict" per file None
useUnknownInCatchVariables catch (e) typed as any Low

If you are migrating an old repo, enable flags one at a time in CI — fix noImplicitAny first, then strictNullChecks. That order reduces the simultaneous error flood.

The 3 errors strict mode catches most often

After dozens of code reviews, these three patterns account for most of the bugs strict mode prevented before merge — not theoretical type pedantry, real user-facing failures.

1. Possibly undefined property access

type User = { id: string; displayName?: string };

function greet(user: User) {
// Error under strictNullChecks: 'displayName' is possibly 'undefined'
return "Hello, " + user.displayName.toUpperCase();
}

// Fix — narrow before use
function greetSafe(user: User) {
const name = user.displayName ?? "Guest";
return "Hello, " + name.toUpperCase();
}

2. Implicit any on event handlers

// BEFORE — noImplicitAny flags the parameter
function handleChange(e) {
setQuery(e.target.value);
}

// AFTER — explicit React type
import type { ChangeEvent } from "react";

function handleChange(e: ChangeEvent<HTMLInputElement>) {
setQuery(e.target.value);
}

3. Wrong API response shape assumed

type ApiOrder = { id: string; total: number; discount?: number };

async function getOrder(id: string): Promise<ApiOrder> {
const res = await fetch("/api/orders/" + id);
return res.json();
}

function OrderSummary({ order }: { order: ApiOrder }) {
// strictNullChecks: discount may be undefined
const saved = order.discount ?? 0;
return <p>Total: ₹{order.total - saved}</p>;
}

Number three is why I type API responses at the boundary — not inside every child component. One wrong assumption on fetch results propagates silently without strict types.

The production bug strict mode would have caught

At my day job, a checkout helper read user.address.line2 without checking if address existed. Indian users who signed up with phone-only onboarding had address: null. The page white-screened on submit.

// BEFORE — passed review, failed in production
type User = { id: string; address: { line1: string; line2?: string } | null };

function formatShipping(user: User) {
return user.address.line1 + ", " + user.address.line2;
}

// AFTER — compiler forces the guard
function formatShipping(user: User) {
if (!user.address) return "Address required";
const line2 = user.address.line2 ?? "";
return user.address.line1 + (line2 ? ", " + line2 : "");
}

We added the guard in a hotfix. With strict null checks enabled from day one, the first version would never have compiled. That single incident paid for every strict migration I have done since.

Migrating a component — before and after strict

// BEFORE — loose props, any sneaks in
export function ProductCard(props) {
const { title, price, onAdd } = props;
return (
<button onClick={() => onAdd(title)}>
{title} — ₹{price}
</button>
);
}

// AFTER — explicit contract
type ProductCardProps = {
title: string;
price: number;
onAdd: (title: string) => void;
};

export function ProductCard({ title, price, onAdd }: ProductCardProps) {
return (
<button onClick={() => onAdd(title)}>
{title} — ₹{price}
</button>
);
}

The after version is longer by four lines. It is also grep-friendly: rename a prop and TypeScript lists every broken callsite instead of leaving undefined at runtime.

Strict mode with Next.js App Router

Next.js 15 projects ship with TypeScript by default. Server Components add one wrinkle: props and params are often Promise-wrapped. Strict typing there prevents awaiting the wrong shape.

// app/blog/[slug]/page.tsx
type PageProps = { params: Promise<{ slug: string }> };

export default async function BlogPostPage({ params }: PageProps) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) return null;
return <Article post={post} />;
}

Pair strict TypeScript with Server Components discipline from my RSC vs client components guide — types tell you what runs where; boundaries tell you what ships to the browser.

When I temporarily relax a rule (rarely)

Third-party libraries with broken types sometimes need @ts-expect-error on one line — not strict: false for the whole project. I also use skipLibCheck: true so node_modules type noise does not block builds — that is standard, not cheating.

// Scoped escape — document why
// @ts-expect-error legacy chart lib ships wrong types until v4
<LegacyChart data={metrics} />

What I do not do: disable strict for "speed." Typing saves more calendar time than it costs once the team is past the first migration week.

My production setup

In production I run strict: true, ESLint @typescript-eslint/recommended, and tsc --noEmit in CI before every merge. On this portfolio and client marketing sites, that combo caught unused env vars and wrong metadata types before they hit Vercel.

When I use AI-assisted refactors, I still read the diff — see my Cursor + Claude workflow — but TypeScript strict mode is the safety net when autocomplete hallucinates a prop name.

Junior developers in Bengaluru often ask if strict is "for senior devs." It is the opposite: strict mode teaches you the shape of data before runtime teaches you with user complaints.

The single takeaway

TypeScript strict mode is cheap insurance. Turn it on in tsconfig.json, fix errors in order, and let the compiler catch null access, implicit any, and API drift before your users do.

Related: Next.js vs React — what to learn first. Performance: How I cut load time by 60% with Next.js. Questions: safdarali.in/contact.

If this helped you

I publish free tutorials and write-ups like this in my spare time — no paywall on the guides. If it saved you an afternoon of trial and error, you can support the work:

More guides on safdarali.in — same author, production-focused.

Top comments (1)

Collapse
 
nazar-boyko profile image
Nazar Boyko

Strict mode still leaves one hole open that fits your empty profile story exactly. Array indexing. With strict on, users[0] is typed as User even when the array is empty, because noUncheckedIndexedAccess isn't part of the strict family and has to be enabled on its own. It's noisy on a legacy repo, but on a greenfield project it closes the same class of crash as your displayName example, just coming from arrays and record lookups instead of optional props. The migration order advice is solid too, noImplicitAny first keeps the error flood survivable.