12 - Tooling: the Strictness Dial
Chapter 12 - Tooling: the Strictness Dial

No troop leaves for camp without the inspection: packing list reviewed in advance, health forms collected, adult ratios checked against the Safety Activity Checkpoints table. The inspection rejects the trip before it starts — a missing form found at the trailhead is a crisis, found at the meeting hall is an errand. Tooling is that inspection for code. The compiler, the formatter, and the linter lay every file on the table before it ships, and each rejects a different family of missing gear. This chapter walks the strictness dial from off to beyond-strict, places lint and format beside the checker, and ends with a lab that turns a loose project strict one flag per commit.
strict true is a family, not a flag
strict: true in tsconfig.json is one setting enabling a family of checks, each documented in the TSConfig Reference. Three members change daily code. noImplicitAny refuses parameters the checker cannot infer — every function boundary must say what it accepts. strictNullChecks makes null and undefined visible inside every union, so the guard’s null arm turns mandatory instead of polite. strictFunctionTypes keeps callback parameters honest. One line in the config, three new errors per old file — and each error is a crash that now happens at the keyboard instead of in a user’s session.
The patrol shows the difference immediately. With strict off, this compiles and lies:
function captainName(patrol) {
return patrol[0].name;
}
// src: assets/typescript/src/decks/tooling/slides/020-strict-core.ts
The parameter is implicitly any, the index read may miss, and the property access assumes. Strict mode rejects all three assumptions at once.
New here: strict, noImplicitAny, strictNullChecks. Docs:
TSConfig Reference.
Watch:
079 — TypeScript Tutorial for Beginners
(the tsconfig tour: target/lib for which JavaScript the
emit may use, outDir/rootDir, and the strict main
switch enabling the core check family — flags stay on, never
disabled).
Try It — Inline: In VS Code, open
tsconfig.json, set"strict": falseand hoverpatrol— no error. Flip totrueand thepatrolparam underlines TS7006 instantly.
Beyond strict: the two flags worth enabling
Past the main switch, two flags earn their keep on nearly every project. noUncheckedIndexedAccess adds undefined to every index read, so patrol[0] becomes Scout | undefined and the empty-patrol case cannot slip past:
const first = patrol[0];
if (first === undefined) throw new Error("empty patrol");
const captain: Scout = first;
The throw narrows the union away and the assignment carries a proven Scout — the narrow-to-prove shape from day 1, now demanded by the config. exactOptionalPropertyTypes stops undefined sneaking past an optional field: declaring essential?: boolean no longer accepts an explicit undefined, so absent means absent — which is exactly what the strict-config lab’s fourth test enforces on shelf stock.
New here: noUncheckedIndexedAccess, exactOptionalPropertyTypes. Docs:
TSConfig Reference — Strictness.
Watch:
007 — Project Structure with tsconfig
(the presenter’s correction that rootDir “says anything
inside src/ is compiled straight into public/ — it does
not say only src/ gets compiled”: config describes the
expected tree; the checker still inspects what strays).
Try It — Inline: In VS Code, type
const x = patrol[0]; const y: string = x;withnoUncheckedIndexedAccesson —yunderlines TS2322. Add theif (x === undefined) throwguard and the error clears without a cast.
Lint the logic, format the layout
The checker proves shapes; the linter flags logic it allows — unused values, needlessly loose comparisons, forgotten promise rejections. The formatter owns layout so reviews argue about meaning instead of commas. The rule for both tools: a warning the team ignores is worse than no rule at all, so every enabled rule either fails the build or gets deleted. This repo runs Biome, one binary doing both jobs — biome check for lint plus format, biome ci in automation — which is the same idea with fewer moving parts. Community types drift too: even the checker needs its own maintenance, and the day the shared types update under a green suite is the day the lockfile earns its keep.
Try It — Inline: In VS Code, write
const _unused = 1and runnpx biome check .— the unused variable lights as a lint error, not a type error. Delete it andbiome checkgoes green.
Three commands, one config
tsc --noEmit is the gate: it proves every type without writing a file, and fails the build on the first lie. Target picks the language level, libraries pick the available APIs, and include/exclude picks which files belong — get those three right and the strict flags have solid ground to stand on. The test runner executes the suite against the same tsconfig the gate used: one config, so green tests and a green gate always agree. The TSConfig Reference documents every knob; the drills below walk the setup the way the videos once did, with the compiler itself as the demonstrator.
Try It — Inline: In VS Code’s terminal, run
npx tsc --noEmit --strict— zero output means green. Introducepatrol[0].namewithout a guard and rerun; the error points at the exact line the editor already underlined.
Production cut: config lookups fall back explicitly
The engine finds its host configuration the way a strict codebase finds everything: by id, with the miss in the type — kept verbatim from the chapter slide:
const config = HOSTS.find((h) => h.id === activeHostId);
return config?.hostType ?? null;
// src: assets/typescript/src/decks/tooling/slides/060-engine-cut.ts
find returns HostConfig | undefined — the miss is declared, not hidden in a comment — and the nullish chain turns it into an explicit null instead of a crash three calls later. Configuration as typed data with an honest default: say what you accept, name the fallback. The hazard that is written down stops being silent, whether it lives in a tsconfig or a host table.
Adopting strict: one flag per commit
Loose codebases go strict one flag per commit: flip the switch, fix exactly the errors it reports, commit while the suite is green, repeat. Strict true first — it reports the honest hazards — then noUncheckedIndexedAccess and exactOptionalPropertyTypes, then lint rules. Change no runtime behavior in any migration commit: the migration renames no variables and moves no logic. The suite stays green throughout, so the migration is shippable at every step and reviewable in pieces. A strictness big bang, by contrast, produces one unreviewable diff and a week of reverts.
DIY simulator drills
You are the simulator. Cover the answers, predict, then verify.
Log each miss in one sentence. One drill per file — Chapter 03
showed why. Drills 1–3 need --target es2020; say the flag
before running.
Drill 1 — Be the boundary guard
Which line errors, and with which code?
function captainName(patrol) {
return patrol[0].name;
}
Verify with npx tsc --noEmit --strict --target es2020.
(With strict off this compiles and lies; with strict on the
untyped parameter errors with TS7006, “implicitly has an
‘any’ type.” Every function boundary must say what it
accepts.)
Drill 2 — Be the index reader
Which line errors under the beyond-strict pair, and with
which code — and which lines error under plain --strict?
declare const patrol: string[];
const first = patrol[0];
const captain: string = first;
Verify both ways with npx tsc --noEmit --strict --target es2020, with and without --noUncheckedIndexedAccess.
(Plain strict: clean — the index read is trusted. With the
flag: the read widens to string | undefined and the
assignment errors with TS2322. The empty patrol cannot slip
past once the flag is on.)
Drill 3 — Be the shelf checker
Which line errors under --exactOptionalPropertyTypes,
and with which code?
type Item = { sku: string; essential?: boolean };
const spare: Item = { sku: "WF-03", essential: undefined };
Verify with npx tsc --noEmit --strict --target es2020 --exactOptionalPropertyTypes. (Optional means the key may
be absent — not that it may carry undefined. The explicit
undefined errors with TS2375. Absent means absent.)
Drill 4 — Be the null spotter
Which line errors under plain --strict, and with which
code?
declare const maybe: string[] | undefined;
const n = maybe.length;
Verify with npx tsc --noEmit --strict --target es2020.
(The union carries its own warning label — reading
.length through a possibly-undefined value errors with
TS18048. The guard’s null arm is mandatory, not polite.)
Lab: turn the dial up (VS Code)
Open static/labs/strict-config/ in VS Code and run setup.sh from the integrated terminal. The catalog ships a small shelf project with strict: false — the checker quiet, the tests loud — and two throwing stubs in catalog.ts. Implement findByName (exact name back with prices intact, strangers back as null, never a throw) and totalEssentials (two flagged essentials out of three items, a missing essential field counting as shelf stock) and go green while the dial is still down (VS Code shows no red squiggles yet); that gap between quiet checker and loud tests is the lesson. Then flip strict to true in VS Code, watch the Problems panel light with TS7006/TS18048, fix exactly what surfaces, enable the beyond-strict pair, and fix again, committing green at every step (npm test && npm run typecheck stays green in the terminal, Biome check stays green). Four tests, zero type errors, no any, no bare casts.