Skip to content

The review checklist

This is the checklist this module has been building toward: one page, six groups, every item phrased as a question you can actually answer by reading a diff — not a vague reminder to “be careful.” Each question links back to the lesson (in this module or an earlier one) that explains exactly why it matters and shows the compiling-but-wrong code it catches.

  • Every as: does it assert a shape that is actually validated somewhere nearby, or is the assertion the only thing standing between this line and a crash? — Typing APIs & Boundaries
  • Every any — explicit, or leaked from an untyped import’s inferred return type: where does it stop, and what crosses that boundary completely unchecked? — Typing APIs & Boundaries
  • Every !: is the non-null claim backed by a nearby runtime check, or by hope? — Nullability, never & Errors
  • Every @ts-ignore or un-coded @ts-expect-error: what error is it hiding, and does removing it still reproduce that error today? — Why TypeScript
  • Every catch (e): is e narrowed (instanceof Error) before .message/.stack is read? — Typing APIs & Boundaries
  • Every Record<string, any> or bare object standing in for real data: would a concrete interface have caught the typo or shape mismatch this code relies on? — Typing APIs & Boundaries
  • Every optional field checked with if (x): does a valid 0, "", or false need to be treated differently from “the field is missing”? — Narrowing
  • Every || used to supply a default: should it be ??, so an explicit falsy value survives? — Nullability, never & Errors
  • Every Readonly<T> / readonly T[]: does the surrounding code (wrongly) assume the protection reaches nested objects too? — Typing APIs & Boundaries
  • Every module-level object literal used as a shared “default”: is it ever mutated by a caller instead of copied? — Typing APIs & Boundaries
  • Every call to an async function or anything else returning a Promise: is it awaited, .catched, or explicitly discarded with void? — Performance & Pitfalls
  • Every Promise.all(...): is the array passed inline (keeping each position’s type), or built in a variable first (which can silently widen to a union)? — Performance & Pitfalls
  • Every switch/if-chain over a union: is there a default branch that assigns the remainder to a never-typed variable? — Nullability, never & Errors
  • Every method read off an instance and passed elsewhere as a bare reference (obj.method, no .bind, no arrow): does its body use this? — this, Polymorphism & Binding
  • Every function parameter typed T[]/Array<T> that only reads the array: could it be readonly T[] instead, closing the covariance-plus-push hole? — Generic Classes & Interfaces — Variance
  • Every hand-written type predicate (x is T): does the function body actually check every field the predicate claims, or does it just return true? — Narrowing
  • Every generic interface/class with exactly one implementer and a type parameter used in exactly one position: does it earn its abstraction, or is it indirection with no reuse payoff? — Generic Functions & Constraints
  • Every Omit/Pick/Exclude applied to something that might be a union: has it actually been checked to distribute the way the author assumed? — The Utility Types
  • Every deeply recursive conditional type over user-controlled input: has it been checked against the compiler’s measured depth limit? — Recursive Types
  • Every enum (numeric or string): does this codebase’s actual runner (node --experimental-strip-types, esbuild, swc) support it, or does it only compile under tsc? — Objects, Arrays & Tuples
  • Every parameter property (constructor(public x: number)): the same question — does the runtime actually used to run this file support the emit it needs? — Build & Emit
  • Every const enum: is it ever imported across a file or package boundary a single-file transpiler will process alone? — Build & Emit
  • Every plain import { X } where X is only ever used as a type: should it be import type { X }? — tsconfig Deep Dive
  • Has tsc --noEmit --strict actually been run on this diff — not just assumed clean because the editor shows no red squiggles? — Verification Tools
  • Has the typed typescript-eslint rule set been run, not just the untyped defaults that ship in most starter configs? — Verification Tools
  • For a published package: does @arethetypeswrong/cli report any resolution or exports problem? — Verification Tools
  • Does knip report any export this diff should have removed, or any dependency it no longer needs? — Verification Tools

Pin this to the top of a PR template — it is the same checklist, compressed to one line per group:

[ ] Boundaries -- every as/any/!/@ts-ignore/catch(e): asserted, or actually checked?
[ ] Nullability -- every falsy-check/||/Readonly/shared-default: does 0 survive, is it deep, is it copied?
[ ] Async -- every promise call: awaited? Promise.all inline? every union switch: never-checked? every bare method ref: bound?
[ ] Generics -- every T[] param: readonly enough? every predicate: really checked? every generic: earning its keep? every Omit/Pick: distributes? every recursion: under the limit?
[ ] Erasure -- every enum/parameter property/const enum: does the actual runtime support it? every type-only import: import type?
[ ] Tooling -- tsc --strict run? typed eslint run? attw/knip run on a package?

And the single grep that surfaces most of the diff worth slowing down for:

grep -nE 'as |: any|!\.|!;|!\)|@ts-|JSON\.parse|\.json\(\)|enum |Object\.keys' <changed-files>
Six groups, one page:
1. Boundaries & assertions -- every place types and reality are allowed to diverge
2. Nullability & defaults -- every place a falsy value or a shared object is easy to mishandle
3. Async -- every place a Promise or a union can be silently mishandled
4. Generics & variance -- every place abstraction either earns its keep or introduces unsoundness
5. Erasure & config -- every place "compiles" and "runs" can diverge
6. Tooling -- did anything besides your own reading actually check this?
Every question is answerable by reading the diff -- none require running the program first.
What you getBenefitCost
A fixed six-group checklistConsistent coverage across every reviewer and every PR, not “whatever I remember to check today”A checklist followed mechanically, without understanding why each item matters, becomes a box-ticking exercise
Questions instead of remindersEach item has a definite yes/no answer readable from the diffSome questions (variance, distribution, recursion depth) need the linked lesson’s background to answer correctly
A one-line-per-group short versionFast enough to actually paste into a PR template and use every timeCompressed enough to lose the “why” — keep the full version linked nearby
A single grep one-linerCatches most of the syntactic hot spots in seconds, before reading a single line of logicA grep hit is not automatically a bug — every hit still needs the judgment the rest of this checklist trains
  • “A checklist replaces judgment.” Every item here still requires reading the specific code around it — the checklist tells you where to look, not what you will find.
  • “If the grep finds nothing, the diff is clean.” The grep list catches syntactic patterns; several patterns in this module’s catalog (a mutable shared default, a lying type predicate, Omit on a union) leave no matching token at all.
  • “Six groups means six separate passes.” In practice, one read of the diff answers most of these questions at once — the six groups organize what to look for, not how many times to read.
  • “This checklist is only for AI-generated code.” Every question here applies equally to a human’s PR; AI-generated code just produces violations at a higher volume and with more confident-looking comments.
Why is each checklist item phrased as a yes/no question ("is e narrowed before .message is read?") rather than a general reminder ("be careful with catch blocks")?
Which checklist group's question would catch that Bare might not mean what its author intended?
type Shape = { kind: "circle"; radius: number } | { kind: "square"; side: number };
type Bare = Omit<Shape, "kind">;
A reviewer runs the grep one-liner from this page's short version and gets zero matches on a diff. What can they conclude?
Why does the checklist separate 'Erasure & config' from 'Boundaries & assertions' as its own group, instead of folding enum/parameter-property/const-enum questions into general type-safety questions?
The Tooling group asks whether the *typed* typescript-eslint rule set was run, specifically calling out that it's different from 'the untyped defaults that ship in most starter configs.' Why does that distinction matter?