14 - Practices: Leave No Trace in Code
Chapter 14 - Practices: Leave No Trace in Code

The last outdoor ethic is the strictest: leave what you find, dispose of waste properly, be considerate of other visitors. A campsite should look untouched when the troop moves on — cleaner, if anything, than it was found. Code follows the same ethic. Types are tools; practices are the habits that keep them sharp and the campsite clean: the idioms worth copying, the anti-patterns worth refusing, the honest middle ground between strictness and pragmatism, and the review checklist that enforces all three. It ends in the refactor lab, where weak code becomes strong without changing its answers.
Idiomatic TypeScript
Idiomatic TypeScript trusts the compiler to do the proving. Five habits carry most of the weight.
Prefer unknown over any at every boundary. any switches the checker off; unknown forces a guard before use. A function that takes unknown announces that strangers arrive here; a function that takes any announces nothing — “I don’t know what the type is, but I will know when I use it” is the whole contract in one sentence.
Narrow instead of casting. typeof checks, in checks, and user-defined guards (value is Scout) prove shapes with code that runs. A bare as asserts without proof — it tells the compiler to believe you, and the compiler keeping score is the entire point of the course. Casts have exactly one honest home: proving something the checker cannot see after a guard already ran, and those cases are rare enough to comment.
Keep unions closed where you own the keys. A Role union with five members is a promise: nothing else is a role. When a stranger role arrives, the guard says null, not maybe. Open shapes (Record<string, string>) belong where the world owns the keys; closed unions belong where you do.
Return fallbacks, never throw, on strangers. null for a missing player, 0 for a stranger count, [] for a stranger list. Throwing is for programmer errors — a contract violated by the caller you control. Strangers are data, and data gets fallbacks.
Freeze what you share. readonly arrays and readonly fields mean the fixture one test reads is the fixture every test reads. Mutation across a shared reference is a whole class of flaky tests deleted in advance.
export function countByRole(patrol: readonly Scout[], value: unknown): number {
if (typeof value !== "string") return 0;
let count = 0;
for (const scout of patrol) {
if (scout.role === value) count += 1;
}
return count;
}
Every habit in one function: stranger as unknown, strict equality instead of casts, a closed union compared member by member, 0 for strangers, and a patrol nobody can mutate.
New here: idioms as enforceable habits. Docs:
Everyday Types.
Watch:
Video 128 — Mistakes Every Junior Developer Should Avoid
(CoderOne’s bad-versus-good tour: unknown over any —
“I don’t know what the type is, but I will know when I use
it” — is predicates over boolean checks plus as casts,
string unions over numeric enums, derived Partial/Omit/
Record over hand-rolled optionality).
Anti-patterns
The same course, viewed from the refuse pile. Each anti-pattern below has a scar behind it from earlier chapters.
any at the boundary. The function accepts anything and proves nothing, so every caller inherits the uncertainty. It typechecks today and fails at runtime in front of users — turning off smoke detectors because toast burned once.
Bare as to silence the checker. The red squiggle was information — the compiler telling you the proof was missing — and the cast deleted it. A cast that silences a stranger check is a guard with a hole cut in it.
Throwing on strangers. A validator that throws on bad input turns data problems into crashes. The loader from chapter 13 never throws on a stranger for exactly this reason; a null page ends the run with what was gathered.
Widening what should stay narrow. Accepting string where only five role codes are legal pushes the stranger check onto every caller instead of settling it once at the seam. Close the union, guard once.
Copying shared data per test. Five private patrol copies drift apart. Fixtures exist so shared data has one address.
Mocking the logic under test. A mock that observes internals breaks on every refactor and teaches nothing. Mocks stand at seams — network, clock, files — and assert on crossings.
New here: the refuse pile as curriculum. Docs:
Strict Mode.
Watch:
Video 116 — TypeScript Anti-Patterns
(Devowski’s seven senior mistakes from “many successful
teams, projects, and products”: “I bet my life at least one
of them applies to 90% of the programmers, even more senior
ones” — stringly-typed domains narrowed to unions,
is-predicate guards, honest naming, atoms-first design).
Strictness versus pragmatism
Strictness is the default; pragmatism is the licensed exception. strict: true stays on in every lab and every starter in this course, because the checker catches whole families of mistakes for free. Pragmatism enters where proof costs more than it returns.
A pragmatic as is allowed when a guard already ran and the checker cannot see the result — narrowing through a library boundary, for instance — and it must carry a comment naming what was proven and where. An uncommented cast is a refusal, not an exception.
// @ts-expect-error beats as for intentional violations: it asserts the error exists, so if a future change removes the error, the directive itself fails loudly instead of rotting silently. Prefer the loud failure.
Skipping strictness for prototypes is honest when it is temporary: spike with loose types to find the shape, then strengthen before the code ships or the tests cite it. What is never honest is loose code with no return ticket: no TODO, no owner, no date. That is not pragmatism; it is drift with a story.
The review checklist
Reviews enforce the practices when memory slips. Every change in this course should clear these six checks; the capstone demo is graded against them directly. Order them by debt contribution — essential checks before cosmetic ones, because completeness is not the goal, sequencing is.
- No
any, no bareas— search the diff, not your memory. - Every boundary input is
unknownuntil a guard proves it. - Strangers get fallbacks (
null,0,[]), never a throw. - Unions are closed where the keys are owned, open where the world owns them.
- Shared data has one address: fixtures, not copies.
npm test && npm run typecheckis green, and one deliberate red proved the tests guard something.
The checklist is short on purpose. Six checks fit in a review comment; sixty would fit nowhere. When a change fails a check, the fix is a refactor, not an argument — the practices above name the shape of every fix in advance.
New here: review as enforcement. Docs:
Vitest Assertion API.
Watch:
Video 130 — The Ultimate Code Review Checklist
(“you must do checks that are more essential before those
that do not contribute significantly to technical debt” —
completeness is not the goal, sequencing is) and
Video 127 — Code Review Fundamentals
(any misuse is “turning off your smoke detectors just
because you burnt toast once”; missing types get fixed
config-level; every as is a red flag; “do these interfaces
actually accurately model the domain?”).
Production cut: the course’s own gate
This course ships the practices it teaches. The pre-commit hook runs the formatter check on staged files, so unformatted code never becomes a commit. Lab suites pair npm test with npm run typecheck, so behavior and types stay green together. The deploy script guards against an empty build before any destructive sync. And the review checklist above is enforced by CONTRIBUTING’s loop: one section per run, drills verified with real tools, both lint gates green, single-file commits. Practices read as style until the gate behind them bites; these gates bite every run.
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.
Drill 1 — Be the overlap checker
Which cast errors, and with which code — and which passes?
const a = "3" as number;
declare const stranger: unknown;
const b = stranger as number;
Verify with npx tsc --noEmit --strict --target es2020.
(The first cast errors with TS2352: string and number
neither sufficiently overlaps, so the conversion “may be a
mistake.” The second passes — unknown converts to
anything, which is exactly why it is the honest boundary
type. Say which errors before running.)
Drill 2 — Be the shape checker
Which declaration errors, and with which code?
interface User {
id: number;
name: string;
}
const c: User = { id: 1 };
const d = { id: 1, name: "Al" } satisfies User;
Verify with npx tsc --noEmit --strict --target es2020.
(The annotation errors with TS2741: name is missing but
required. The satisfies form passes and keeps the
narrowed literal types while staying contract-checked —
annotation widens, satisfies proves.)
Drill 3 — Be the directive checker
Which line errors, and with which code?
// @ts-expect-error
const e: string = "fine";
Verify with npx tsc --noEmit --strict --target es2020.
(The assignment is fine, so there is no error to expect —
the unused directive itself errors with TS2578. The
directive asserts an error exists; asserting what is not
there fails loudly. That loudness is the feature.)
Drill 4 — Be the refactor
What prints?
function totalMiles(rows) {
let total = 0;
for (const row of rows) {
if (typeof row.miles === "number") total += row.miles;
}
return total;
}
console.log(totalMiles([{ miles: 3 }, { miles: "x" }, {}, { miles: 5 }]));
Verify with node. (8 — the two real mileages sum, the
string and the missing field contribute nothing. The guard
inside the loop is the whole refactor in miniature: prove
each row, skip strangers silently. Say the number before
running.)
Lab: refactor weak-to-strong
Open static/labs/refactor-weak-strong/ and run setup.sh. The before file is weak and unfinished: three exported names with any at every seam, each a stub that throws. Rewrite src/lineup.ts so parseEntry proves trail entries ({ name: string; miles: number }) and returns null for strangers, totalMiles sums proven rows and counts strangers as 0, and leaders names the top N highest-first with [] for stranger counts. Keep the exported names; change everything else. Five tests wait, including caller narrowing. Go green with npm test && npm run typecheck, then compare with the solution: same behavior, possibly different shape. Green plus agreement is done.