TypeScript Survival Guide

Course manual

12 - Tooling: the Strictness Dial

Chapter 12 - Tooling: the Strictness Dial

Scouts inspecting gear with checklist and strictness dial at the trailhead

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": false and hover patrol — no error. Flip to true and the patrol param 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; with noUncheckedIndexedAccess on — y underlines TS2322. Add the if (x === undefined) throw guard 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 = 1 and run npx biome check . — the unused variable lights as a lint error, not a type error. Delete it and biome check goes 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. Introduce patrol[0].name without 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.