TypeScript63 min total · 21 parts
TypeScript Fundamentals: Types, Interfaces, Generics, and Why It Catches Bugs Before Runtime
Part 19 of 21 · ~3 min
Configuring tsconfig.json: Strict Mode and Friends
Everything covered so far can be turned mostly off. tsconfig.json is where that decision actually gets made, and a project running with the loosest settings can look, on the surface, almost nothing like the same project running with the strictest ones — most of what actually makes TypeScript worth adopting sits behind flags nobody flips on for you.
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"strict": true,
"noUncheckedIndexedAccess": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
}
}
strict: true isn't one setting — it's shorthand for a group of them, each independently toggleable:
Setting, bundled under strict | What it stops you from doing |
|---|---|
noImplicitAny | Letting an unannotated, uninferable value quietly become any |
strictNullChecks | Treating null/undefined as assignable to a type that never said it accepted them — probably the single most consequential setting on this list |
strictFunctionTypes | Assigning a function where its parameter types don't actually line up safely |
strictPropertyInitialization | Declaring a class field with no ?, no !, and no actual assignment anywhere in the constructor |
useUnknownInCatchVariables | Getting any instead of unknown in a catch block — the gap from a couple of chapters back |
alwaysStrict | Skipping "use strict" and sloppy-mode parsing |
Time to actually go back to the very first paragraph of this reference:
interface Reviewer { email: string; }
function getNextApplicant(queue: ApplicationQueue): Applicant | undefined {
return queue.dequeue();
}
function notifyReviewer(queue: ApplicationQueue, reviewer: Reviewer) {
const next = getNextApplicant(queue);
sendEmail(reviewer.email, `Review ${next.name}`, `/applicants/${next.id}`);
// no strictNullChecks: this compiles, exactly like the plain-JS version at the top of this reference did
// strictNullChecks on: Error: 'next' is possibly 'undefined'
}
Without strictNullChecks, getNextApplicant's honest Applicant | undefined return type is decoration — null and undefined are quietly accepted everywhere, so next.name compiles exactly as happily as it did back when this whole file was plain JavaScript, and the Friday-afternoon incident is one bad deploy away from happening again, this time in a codebase that technically had the right type written down the entire time. Flip strictNullChecks on, and the exact same line refuses to compile until the empty case is actually handled:
function notifyReviewer(queue: ApplicationQueue, reviewer: Reviewer) {
const next = getNextApplicant(queue);
if (!next) return; // no longer optional — the compiler will not let this be skipped
sendEmail(reviewer.email, `Review ${next.name}`, `/applicants/${next.id}`);
}
Two separate things had to be true for that setting to have actually prevented the real incident. strict had to be turned on in the first place — retrofitting it onto a codebase that grew up without it is real, often painful work, which is the whole argument for starting a new project with strict: true on day one rather than promising to add it "later." And the earlier warning from the very first chapter applies here too: tsc had to be a required step in the deploy pipeline — something like tsc --noEmit that a bad build genuinely cannot get past — rather than a warning that lived only in an editor, easy to scroll past on the way to committing. A type error nobody is forced to look at protects nobody.
noUncheckedIndexedAccess sits outside strict but is worth turning on for the same reason. Without it, applicants[i] is typed as Applicant, full stop — even though applicants[999] on a five-element array is completely ordinary JavaScript that hands back undefined at runtime. Same failure mode as the queue bug above, just reached through indexing instead of dequeue.