Turn on TypeScript strictness without a big-bang break. The skill measures where you stand (explicit any, implicit any, which strict flags are already on), then enables the strict family one sub-flag at a time — noImplicitAny, strictNullChecks, and the rest — fixing the fallout from each flag before touching the next. Every step ends with tsc --noEmit passing, so you ratchet coverage up monotonically instead of staring at 600 errors and rage-casting them away.
When to use this skill
- The codebase runs with
strict: false(or a partial strict config) and is littered withany, implicitanyparameters, and unchecked nullables. - You want to reach
strict: truebut a single flip produces an unfixable wall of errors and a stalled PR. anyis masking real defects —undefined is not a function, missing-property crashes — that strict typing would have caught at compile time.
[!WARNING] Do not "fix" strict errors with
any-casts,asassertions,@ts-ignore, or@ts-expect-error. Those silence the exact diagnostic strict mode exists to surface — you ship the bug and the suppression. The only acceptable fixes are: a precise type, a real null/undefined narrow, or (rarely) a documented// @ts-expect-errorwith a linked issue when the fix is genuinely a separate change. A PR whose net effect is "more suppressions" is negative progress.
Instructions
Measure the baseline before changing anything. Read
tsconfig.jsonand record which strict sub-flags are already set (strictimpliesnoImplicitAny,strictNullChecks,strictFunctionTypes,strictBindCallApply,strictPropertyInitialization,noImplicitThis,useUnknownInCatchVariables,alwaysStrict). Then quantify theanysurface:# explicit `any` annotations grep -rIn -E ':\s*any\b|\bas any\b|<any>|Array<any>|any\[\]' --include='*.ts' --include='*.tsx' src | wc -l # existing suppressions (these are debt you must not add to) grep -rIn -E '@ts-ignore|@ts-expect-error' --include='*.ts' --include='*.tsx' src | wc -l # implicit any + the full error count under the strictest config (dry run, no edits) npx tsc --noEmit --strict --noErrorTruncation 2>&1 | grep -c 'error TS'If
type-coverageis available,npx type-coverage --detailgives a single percentage and a per-identifier list — capture the starting number; it is your headline metric.Order the work by risk and traffic, not alphabetically. Use
git log --format= --name-only --since='6 months ago' | sort | uniq -c | sort -rnto find churned files, and grep for the modules with the mostanyand the most importers (entry points, shared utils, API/DB boundaries). Fix these first: a precise type on a widely-imported util propagates correctness everywhere; ananyat a data boundary (HTTP response, DB row, JSON parse) is where wrong-shape bugs originate.Enable exactly one sub-flag at a time. Add a single flag to
tsconfig.json("noImplicitAny": true), runnpx tsc --noEmit, and fix only the errors that flag produces. Recommended order, easiest-to-hardest:noImplicitAny— annotate parameters/returns the compiler couldn't infer.strictNullChecks— the big one; surfaces every placenull/undefinedwas silently allowed.strictFunctionTypes,strictBindCallApply,noImplicitThis— usually small fallout.strictPropertyInitialization— class fields; often the last and noisiest. Once each flag is green individually, the final flip to"strict": trueis a no-op verification.
Replace
anywith the real type, narrow at the boundary. For explicitany: infer the actual shape from how the value is used and from the producer, and write theinterface/type. For external/untyped data (JSON.parse,fetch().json(), env vars, dynamic imports), type the boundary asunknownand narrow with a type guard or a schema parse (e.g.zod's.parse()) —unknownforces a check;anyskips it. Add explicit return types to exported functions so inference errors surface at the definition, not three call sites away.Keep the typecheck green at every commit. After each flag's fallout is fixed, run the project's real check (
npm run typecheck/tsc --noEmit) and the test suite, then commit that flag as its own commit. Never enable the next flag on a red tree — you lose the ability to attribute a new error to a specific flag, and the diff becomes unreviewable.Re-measure and report the delta. Re-run the baseline commands from step 1. Report the before/after
anycount, thetype-coveragepercentage delta, which flags are now on, and any honest residue: spots that genuinely needunknown+ a follow-up, third-party@typesgaps, or generated code excluded viatsconfigexcluderather than suppressed inline.
[!NOTE] Don't refactor logic while fixing types. A type-only PR should change annotations, guards, and config — not behavior. If a strict error reveals a real bug (a nullable that was actually being dereferenced), fix it in a separate commit with a test, so reviewers can tell "added a type" apart from "changed runtime behavior."
Output
Baseline metrics — current
tsconfigstrict flags, explicit-anycount, suppression count, total error count under--strict, andtype-coveragepercentage if available.An ordered flag-by-flag plan — the sub-flags to enable in sequence, each with its estimated fallout count and the highest-priority files to fix first, e.g.:
Step Flag Errors introduced Fix-first files 1 noImplicitAny38 src/lib/api/client.ts,src/utils/parse.ts2 strictNullChecks142 src/db/repository.ts,src/lib/session.ts3 strictPropertyInitialization21 src/services/*.tsConcrete type changes for the first file — the actual diff:
any→ named types, added return annotations, andunknown-at-the-boundary guards, withtsc --noEmitshown passing afterward. For example:- export function parseUser(raw: any) { - return { id: raw.id, name: raw.name }; - } + interface User { id: string; name: string } + export function parseUser(raw: unknown): User { + if (typeof raw !== "object" || raw === null) throw new Error("invalid user"); + const r = raw as Record<string, unknown>; + if (typeof r.id !== "string" || typeof r.name !== "string") throw new Error("invalid user"); + return { id: r.id, name: r.name }; + }$ npx tsc --noEmit $ # exit 0 — clean