tsconfig essentials — strict mode and what it actually catches
A tsconfig.json without strict mode enabled lets a lot of the checking this entire domain has covered simply not happen. Understanding what strict actually turns on — and which single flag inside it matters most — is the difference between TypeScript that catches real bugs and TypeScript that's mostly decorative.
4 min read
tsconfig.json: one file, controlling how the whole project is checked
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"strict": true,
"outDir": "./dist"
},
"include": ["src/**/*.ts"]
}Every real TypeScript project has a tsconfig.json at its root — the single file controlling how tsc (and every editor's TypeScript integration, which reads the same file) checks and compiles every .ts file in the project. target sets which JavaScript version the output is compiled down to; module sets the module system (ESNext, CommonJS); include/exclude control which files are actually part of the compiled project at all. The one setting worth understanding in real depth is strict.
strict: true isn't one flag — it's a bundle of several
{
"compilerOptions": {
"strict": true
// equivalent to enabling ALL of:
// strictNullChecks, noImplicitAny, strictFunctionTypes,
// strictBindCallApply, strictPropertyInitialization,
// noImplicitThis, alwaysStrict, useUnknownInCatchVariables
}
}strict: true is shorthand that turns on a whole family of individually-toggleable checks at once — each of which can also be set independently if a project needs more granular control (common when incrementally migrating a large JavaScript codebase to TypeScript, turning strict checks on one at a time). New TypeScript projects should start with strict: true from day one — retrofitting it onto a large codebase written without it later means confronting every place strict mode's checks would have caught something, all at once, which is a genuinely large, disruptive undertaking compared to having it on from the start.
strictNullChecks: the single highest-value flag inside strict
// WITHOUT strictNullChecks — this compiles fine, and crashes at runtime
function getUser(id: number): { name: string } {
if (id < 0) return null; // no error — null is silently assignable to ANY type
return { name: "Ada" };
}
getUser(-1).name; // runtime crash: Cannot read properties of null
// WITH strictNullChecks — the mistake is caught at compile time instead
function getUser(id: number): { name: string } | null {
if (id < 0) return null; // now this requires the return type to explicitly include | null
return { name: "Ada" };
}
getUser(-1).name; // Error: 'getUser(-1)' is possibly 'null'Without strictNullChecks, null and undefined are silently assignable to every type — a function declared to return { name: string } can secretly return null with no compile error at all, and the mistake only surfaces later as a genuine runtime crash, at whatever line first tries to use the value. With it enabled, null/undefined have to be explicitly included in a type (| null) wherever they're genuinely possible, and TypeScript then forces every consumer of that value to handle the null case (via narrowing) before using it normally. This single flag is widely considered the most valuable individual check inside strict — it directly targets TypeError: Cannot read properties of undefined/null, one of the single most common runtime crashes in real, unguarded JavaScript.
noImplicitAny: catching the silent fallback the basic-types lesson mentioned
// WITHOUT noImplicitAny — x silently becomes `any`, no annotation, no error
function double(x) {
return x * 2;
}
// WITH noImplicitAny — this is now a compile error, forcing an explicit type
function double(x) { // Error: Parameter 'x' implicitly has an 'any' type
return x * 2;
}This is the flag that turns the basic-types lesson's "an unannotated parameter silently becomes any" fallback into a hard compile error instead — forcing every parameter (and a few other genuinely ambiguous positions) to be given a real, explicit type rather than quietly falling through to the type that disables checking entirely. Combined with strictNullChecks, these two flags alone account for the overwhelming majority of real bugs a well-configured TypeScript project actually catches.
Other project settings worth knowing, briefly
{
"compilerOptions": {
"esModuleInterop": true, // smooths over CommonJS/ES module import mismatches
"skipLibCheck": true, // skips type-checking .d.ts files themselves — much faster builds
"isolatedModules": true, // required by tools like esbuild/SWC — rules out const enum, among others
"noUnusedLocals": true, // flags variables declared but never used
"resolveJsonModule": true // allows importing .json files directly, with inferred types
}
}None of these carry the same weight as strict, but each solves a genuinely common, real friction point: esModuleInterop fixes a very common class of import error between CommonJS and ES module packages; skipLibCheck is almost always turned on in real projects purely for build speed, since re-checking every dependency's own already-published .d.ts files on every build is expensive and rarely catches anything actionable; isolatedModules is the flag that explicitly forbids const enum (from the enums lesson) — worth recognizing by name, since it's often the actual cause when a const enum mysteriously stops compiling in a specific build tool.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Is `strict: true` in tsconfig.json a single check, or something else?
2. Without `strictNullChecks`, what happens if a function declared to return `{ name: string }` actually returns `null` in some branch?
3. What does `noImplicitAny` change about an unannotated function parameter?