Skip to main content
CodeOath
← All posts

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 strictWhat it stops you from doing
noImplicitAnyLetting an unannotated, uninferable value quietly become any
strictNullChecksTreating null/undefined as assignable to a type that never said it accepted them — probably the single most consequential setting on this list
strictFunctionTypesAssigning a function where its parameter types don't actually line up safely
strictPropertyInitializationDeclaring a class field with no ?, no !, and no actual assignment anywhere in the constructor
useUnknownInCatchVariablesGetting any instead of unknown in a catch block — the gap from a couple of chapters back
alwaysStrictSkipping "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.