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:
- → Buy me a coffee at buymeacoffee.com/safdarali
- 👉 Subscribe to my YouTube channel — it's free; 70+ React & Next.js tutorials
Related reading
More guides on safdarali.in — same author, production-focused.
-
How to Build a Frontend Developer Portfolio That Stands Out
Frontend developer portfolio guide for India — sections, React/Next.js examples, SEO, performance, personal branding, FAQ, and checklist to build and rank.
May 2026 · Read article →
-
React Server Components vs Client Components — When to Use Which
Practical RSC vs client guide for Next.js App Router — when to use each, real code, bundle before/after, and performance impact.
May 2026 · Read article →
Top comments (1)
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.