All posts
typescripttsconfig

TSConfig Deep Dive: A Practical Guide for Full-Stack Developers

A practical guide to tsconfig.json — the compiler options that actually matter, common misconfigurations, and a production-ready baseline.

SR

Suhail Roushan

August 6, 2026

·
4 min read
·
0 views

Most tsconfig.json files in production are copy-pasted from a starter template and never touched again, and that's how bugs that TypeScript should have caught slip straight into production.

A tsconfig.json deep dive matters because this one file decides how strict, how fast, and how compatible your TypeScript compiler actually is. Two projects can use the exact same TypeScript version and catch a completely different set of bugs, purely because of which flags are turned on. If you've ever wondered why a coworker's editor flags an error yours doesn't, the answer is almost always sitting in this file.

Why Your TSConfig Matters (and When to Skip Customizing It)

The default tsconfig.json generated by tsc --init is permissive on purpose — it's meant to ease migration from JavaScript, not to catch every bug. Left as-is, any sneaks in everywhere and strict mode is off. If you're building anything beyond a quick script, the defaults are actively working against you.

Skip deep customization only for throwaway scripts or prototypes where type safety isn't the point. For anything shipping to users, invest the ten minutes.

Getting Started with a Production TSConfig

Here's a baseline that works for most Next.js/Node backend projects:

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022", "DOM"],
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

strict: true alone enables seven separate checks (noImplicitAny, strictNullChecks, strictFunctionTypes, and more) — turning it on is the single highest-leverage change you can make to an existing config.

Core TSConfig Concepts Every Developer Should Know

strict mode is a bundle, not one flag. Under the hood it flips on noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, and alwaysStrict. You can override any individual one, but starting from strict: true and opting out is safer than starting from false and opting in.

moduleResolution decides how imports resolve. "Bundler" (the modern default, matching how Vite/Next.js/esbuild actually resolve modules) understands package.json exports fields correctly. The older "Node" setting predates that and will misresolve packages that only ship modern exports maps.

noUncheckedIndexedAccess closes a real hole. Without it, arr[i] is typed as T, even though arr[i] can legitimately be undefined at runtime for an out-of-bounds index:

const users: string[] = ["Suhail"];
const third = users[2]; // typed as `string` without the flag — wrong, it's undefined

With the flag on, third is correctly typed string | undefined, forcing you to handle it.

skipLibCheck trades a small safety net for real speed. It skips type-checking .d.ts files in node_modules, which is almost always safe since you don't control those files anyway, and can meaningfully cut compile time on large projects.

Common TSConfig Mistakes and How to Fix Them

Mistake 1: leaving strict off "for now." It never gets turned on later — the codebase accumulates implicit any and null-unsafe code that makes flipping it on later a multi-day cleanup. Fix: turn it on from day one, even in a prototype.

Mistake 2: mismatched target and runtime. Setting target: "ES5" while deploying to modern Node.js means the compiler downlevels syntax you didn't need downleveled, bloating output and sometimes changing runtime behavior around this. Fix: match target to your actual deployment runtime — ES2022 for any modern Node.js or edge runtime.

Mistake 3: include/exclude too broad, causing IDE slowdowns. Including node_modules or generated build output in include makes the language server crawl thousands of unnecessary files. Fix: scope include to your actual source directory and always exclude dist/build/node_modules.

When Should You Write a Custom TSConfig Instead of Using the Default?

The moment your project has more than a handful of files, or ships to real users. A default config catches maybe 60% of the bugs strict: true catches — the gap is exactly the kind of null-reference and implicit-any bugs that show up as production incidents, not compiler errors.

TSConfig in Production

Monorepos should use a shared base tsconfig.base.json that each package extends, so strictness settings can't silently drift between packages. And when adopting strict mode on an existing loose codebase, do it incrementally with strict: false at the root and per-directory overrides — trying to fix every violation in one PR is how strict mode adoption stalls forever.

Turn on strict: true in your next project before writing a single line of app code — retrofitting it later is always more expensive than starting with it.

Related posts

Written by Suhail Roushan — Full-stack developer. More posts on AI, Next.js, and building products at suhailroushan.com/blog.

Get in touch