tsconfig.json — The Control Room
TypeScript's default settings are dangerously lenient. Learn how to configure tsconfig.json to unlock the compiler's full protective power and catch bugs before they ship.
📖 The Story: The Defaults Trap
A startup launched their TypeScript project with the default tsconfig.json generated by tsc --init. The comments showed dozens of options, but none were enabled. The team assumed "TypeScript is keeping us safe."
Six months later, a production crash occurred. The root cause? A function received null for a parameter that the developer assumed was always a string. The code was TypeScript—but strictNullChecks was off, so TypeScript happily allowed null anywhere.
The CTO discovered that without strict: true, TypeScript was running in "legacy mode"—barely safer than plain JavaScript. They flipped one boolean in tsconfig.json, and the compiler immediately surfaced 847 errors across the codebase. Each one was a potential crash waiting to happen.
It took two weeks to fix those 847 errors. The team hasn't had a null-related production incident since.
TypeScript without strict mode is a seatbelt you never buckle. It looks safe but protects nothing.
🎯 Why tsconfig Matters
Safety Level
strict: true transforms TS from "JavaScript with autocomplete" to a genuine bug-catcher.
Build Output
Controls what JavaScript version is generated and how modules are bundled.
Developer Experience
Path aliases eliminate ../../../ import chains forever.
🧒 Explain Like I'm 10
Imagine a super-smart robot (the TypeScript compiler) that checks your homework. The tsconfig.json file is the rules sheet you give the robot.
If the rules sheet says "just glance at it" (no strict), the robot only catches obvious mistakes. If the rules sheet says "check EVERYTHING, letter by letter" (strict mode), the robot catches every tiny error.
Most people leave the rules sheet nearly blank and wonder why the robot misses things. Fill it out properly, and the robot becomes unstoppable.
🎓 Professional Explanation
tsconfig.json is the configuration file for the TypeScript compiler (tsc). It's divided into two main sections: compilerOptions (how the compiler behaves) and top-level fields like include/exclude (which files to compile). The strict flag is an umbrella that enables eight individual strictness flags, each catching a different category of bug.
⚙️ Anatomy of tsconfig
{
"compilerOptions": {
/* Language & Environment */
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2022", "DOM"],
/* Strictness */
"strict": true,
/* Module Resolution */
"moduleResolution": "bundler",
"baseUrl": "./",
"paths": { "@/*": ["./src/*"] },
/* Output */
"outDir": "./dist",
"rootDir": "./src",
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
🛡️ Strict Mode Family
Setting "strict": true enables all eight flags below. Let's see what each one protects you from:
1. strictNullChecks — The Most Important Flag
// WITHOUT strictNullChecks: function greet(name: string) { return "Hello " + name.toUpperCase(); } greet(null); // No error! Crashes at runtime: Cannot read .toUpperCase of null // WITH strictNullChecks: function greet(name: string) { return "Hello " + name.toUpperCase(); } greet(null); // ❌ Error: Argument of type 'null' is not assignable to 'string' greet(undefined); // ❌ Error too!
This single flag eliminates the #1 crash in JavaScript applications: null and undefined access errors.
2. noImplicitAny — No Silent any
// WITHOUT noImplicitAny: function process(data) { // data is silently 'any' return data.foo.bar.baz; // No warning! Crashes if data is null } // WITH noImplicitAny: function process(data) { // ❌ Error: Parameter 'data' implicitly has 'any' type return data.foo.bar.baz; }
3. The Other Six Flags
| Flag | What It Catches |
|---|---|
strictFunctionTypes | Contravariant function parameter checking (sound function types) |
strictBindCallApply | Correct types for bind(), call(), apply() |
strictPropertyInitialization | Class properties must be initialized in constructor |
noImplicitThis | this in functions must have a known type |
alwaysStrict | Adds "use strict" to output |
useUnknownInCatchVariables | catch (e) gives unknown, not any |
💎 Hidden Gems Beyond Strict
These flags aren't in strict but should be enabled in every serious project:
noUncheckedIndexedAccess
// Without this flag: const arr = [1, 2, 3]; const item = arr[10]; // Type: number (but it's actually undefined!) // With noUncheckedIndexedAccess: const item = arr[10]; // Type: number | undefined ✅ item.toFixed(2); // ❌ Error! Might be undefined if (item !== undefined) { item.toFixed(2); // ✅ Safe! }
This flag also applies to object index access with Record<string, T>, making myRecord[key] return T | undefined.
Other Essential Non-Strict Flags
{
"noUncheckedIndexedAccess": true, // Array/object access includes undefined
"noUnusedLocals": true, // Error on unused local variables
"noUnusedParameters": true, // Error on unused function parameters
"noFallthroughCasesInSwitch": true, // Error on switch fallthrough
"noImplicitOverride": true, // Require 'override' keyword in subclasses
"forceConsistentCasingInFileNames": true // Fix Mac/Windows case issues
}
📦 Module Resolution
| Strategy | Use When |
|---|---|
"bundler" | Vite, webpack, esbuild (modern default) |
"NodeNext" | Node.js with ESM or CommonJS |
"node" | Legacy Node.js CommonJS (deprecated) |
🗺️ Path Aliases
The developer experience upgrade that eliminates import spaghetti:
// Before: Import spaghetti import { Button } from '../../../../components/Button'; // After: With path aliases in tsconfig.json import { Button } from '@/components/Button'; /* tsconfig.json: { "compilerOptions": { "baseUrl": "./", "paths": { "@/*": ["./src/*"] } } } */
TypeScript knows the alias, but your bundler (Vite/webpack) doesn't. You must also configure the alias in vite.config.ts or webpack.config.js, otherwise the build will fail.
🏆 The Production-Ready Config
Here's a battle-tested tsconfig.json for a modern frontend project:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
/* Maximum Safety */
"strict": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
/* Module System */
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true,
"skipLibCheck": true,
/* Path Aliases */
"baseUrl": "./",
"paths": {
"@/*": ["./src/*"]
},
/* Output */
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"],
"exclude": ["node_modules"]
}
noEmit: true: Modern bundlers (Vite, Next.js) handle the transpilation. TypeScript just checks types. skipLibCheck: true: Skips checking .d.ts files in node_modules for massive speed improvements.
⚠️ Common Mistakes
Mistake 1: Leaving strict Off "Temporarily"
Every project that starts loose stays loose. The longer you wait, the more errors accumulate, making the migration harder. Enable strict: true from day one.
Mistake 2: Using @ts-ignore Instead of Fixing Types
// ❌ Bad: Silences the error AND hides future bugs on that line // @ts-ignore config.setup(maybeNullValue); // ✅ Better: Scoped, documented suppression // @ts-expect-error: Third-party lib hasn't updated types yet (Issue #123) config.setup(maybeNullValue);
@ts-expect-error is better than @ts-ignore: It errors if the underlying issue is fixed (reminding you to remove the comment). @ts-ignore silently suppresses forever.
Mistake 3: Forgetting extends for Monorepos
// packages/shared/tsconfig.json (base config) { "compilerOptions": { "strict": true, "target": "ES2022" } } // apps/frontend/tsconfig.json (extends base) { "extends": "../../packages/shared/tsconfig.json", "compilerOptions": { "jsx": "react-jsx" // App-specific override } }
✅ Best Practices
- Always use
strict: true— non-negotiable for new projects. - Add
noUncheckedIndexedAccess: truefor maximum safety on array/object access. - Use
skipLibCheck: truefor faster compilation on large projects. - Use path aliases (
@/*) to keep imports clean and refactoring easy. - Extend a base config in monorepos to keep all packages consistent.
- Prefer
@ts-expect-errorwith a reason comment over@ts-ignore.
🏗️ Senior Deep Dive
The extends Chain
TypeScript supports cascading extends up to 5 levels deep. Frameworks like Next.js and Vite provide their own base configs (next-env.d.ts or vite/client types). Understanding how these merge is critical for debugging why a type isn't resolving.
tsconfig vs Project References
For massive codebases, Project References ("references": [{"path": "./packages/core"}]) allow incremental compilation—only rebuilding changed packages. This turns a 60-second compile into a 5-second one.
💼 Interview Questions
strict: true enable?Show Answer
Show Answer
@ts-ignore suppresses any error on the next line forever. @ts-expect-error suppresses the error but will itself error if no error exists—reminding you to remove the comment when the underlying issue is fixed.Show Answer
noEmit: true prevents the compiler from generating .js files, speeding up the check and avoiding conflicts with the bundler's output.Show Answer
noImplicitAny (usually catches the most errors but easiest to fix with types), then strictNullChecks (catches the most dangerous bugs). Tools like ts-strictify can count remaining errors per flag. The alternative—shipping without strict—costs more in production incidents than the migration effort.🏋️ Practical Exercise
Audit Your tsconfig
- Open any TypeScript project's
tsconfig.json. - Check: Is
strict: trueset? If not, you've found a vulnerability. - Check: Is
noUncheckedIndexedAccessenabled? - Count how many
@ts-ignorecomments exist:grep -r "@ts-ignore" src/ | wc -l - Enable strict mode and see how many errors surface. Each one is a prevented bug.
📝 Quiz
Test your understanding. Click an option to check your answer.
📦 Summary
🎯 Key Takeaways
strict: trueis non-negotiable — it enables 8 safety flags including strictNullChecks.noUncheckedIndexedAccessmakes index access returnT | undefined.- Path aliases (
@/*) eliminate deep relative imports. noEmit: truewhen a bundler handles transpilation.skipLibCheck: truefor faster compilation on large projects.- Prefer
@ts-expect-errorover@ts-ignorewith a reason comment. - Use
extendsto share configs across monorepo packages.
Comments
Comments
Post a Comment