Type Coverage Improver
Raise TypeScript type strictness incrementally — measure the any/implicit-any baseline, enable one strict sub-flag at a time, and fix the fallout per flag instead of all at once, keeping the typecheck green at every step. Use when a codebase is loosely typed, when you want strict mode on without a big-bang break, or when `any` keeps hiding bugs that surface in production.
npx agentscamp add skills/type-coverage-improverInstall to ~/.claude/skills/type-coverage-improver/SKILL.md
Flipping `strict: true` on a loose codebase floods you with hundreds of errors at once and tempts blanket `any`-casts that defeat the point. This skill makes the climb incremental: count the explicit/implicit `any` baseline, enable strict sub-flags one at a time (highest-traffic files first), and fix the fallout flag-by-flag while the typecheck stays green throughout.
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, as assertions, @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-error with 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/*.ts -
Concrete 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
Related
- Dead Code FinderFind genuinely unused code — unreferenced exports, unreachable files, and unused dependencies — and remove it safely with build/test verification. Use when trimming a codebase or untangling years of accreted cruft.
- Extract ModuleSplit an overgrown file into cohesive, well-bounded modules — find the natural seams, design each new module's public interface before moving a line, then relocate one unit at a time keeping tests green. Use when a file has grown too large, mixes unrelated responsibilities, or every change to it forces unrelated diffs and merge conflicts.
- Trace Data FlowTrace how a value, field, or variable flows through the codebase from source to sink.
- React Render ProfilerFind and fix wasteful React re-renders by classifying the cause — unstable prop/callback/object identities, context value churn, state lifted too high, expensive work in render, or unvirtualized lists — confirming it with a measurement, then applying the one targeted fix and re-measuring. Use when a React UI is janky, slow to type in, or re-renders far more than the data actually changed.
- Circular Dependency BreakerDetect and break a circular import — map the exact cycle with a real tool, then break the right edge by extracting the shared piece into a leaf module, inverting a layering dependency, merging two falsely-split modules, or (last resort) deferring an import. Use when you hit an import cycle error, an undefined-on-import or 'cannot access before initialization' bug, or a bundler/linter flags a cycle.