Tighten the static typing in $ARGUMENTS without changing runtime behavior and without breaking the build.
If $ARGUMENTS is empty, type-check the whole project, then scope the work to files with any, non-null assertions (!), or as casts — prioritize the ones on hot paths and public exports. If it names a file, directory, or symbol, scope to that.
Method
- Detect conventions first: read
tsconfig.json(isstrict/noUncheckedIndexedAccess/exactOptionalPropertyTypeson?), the lint config, and neighboring files. Match the codebase's style (type vs interface,readonlyusage, zod/valibot schemas). Do not introduce a new pattern the project doesn't already use. - Establish a baseline: run the typechecker over the correct scope and record its error count and the exact command. Prefer the project's own script (
npm run typecheck); for a single tsconfig usetsc --noEmit -p <tsconfig>; for a monorepo with project references (referencesarray, orcomposite: true) usetsc -b, and pick the tsconfig that actually covers $ARGUMENTS — never a root config that skips the target's scope. If the baseline is already red, that is not license to add errors: leave those pre-existing errors reported but no worse, introduce zero new errors, and reduce the count wherever your edits touch failing code. - Fix in this order, re-checking after each cluster:
- Replace
anywith the real type. If genuinely unknown at a boundary, useunknownand narrow at the point of use — never leaveany. - Remove unsafe casts.
as SomeTypeandas unknown as Thide bugs; replace with a type guard, a discriminated check, or a correct annotation. Keepas constand casts to a strict subtype that the compiler can't infer but you can prove. - Remove non-null
!. Prove non-null with a guard, early return, or invariant/assert helper that throws. - Model the data precisely: make illegal states unrepresentable. Use discriminated unions (shared literal
kind/typetag) over optional-bag objects;T | nullover sentinels; template-literal and branded types where the project already does. - Add generics to eliminate duplication and preserve the input→output type relationship. Prefer inference; add constraints (
extends) instead of casting inside the body. Do not add type parameters that appear only once. - Narrow safely with user-defined type guards (
x is T),in,typeof,instanceof, and exhaustiveswitchclosed by aneverdefault. At external boundaries (network, JSON, env) parse untrusted data through a runtime validator that returns a typed value (zod/valibot.parse); neveras-assert it into a type, since a cast validates nothing at runtime.
- Replace
- Prefer
satisfies Tto check a value against a type while keeping its narrow inferred type instead of annotating-and-widening; preferPick/Omit/Parameters/ReturnType/mapped types over restating shapes. Turn magic strings into unions. - Re-run the exact baseline command until it reports no new errors and a count <= baseline (green if the baseline was green). Run the linter and any affected tests. Never suppress with
@ts-ignore/@ts-expect-error/eslint-disableto reach that bar — if a suppression is truly unavoidable, use@ts-expect-errorwith a one-line reason and flag it.
Output
- A short summary: files touched, and counts of
any/as/!removed. - For each non-trivial change, a before/after diff with one line on why the new type is safer.
- The final typechecker command and its clean result.
- A "Left alone" list: any cast/
anyyou kept, with the reason it's load-bearing.
Rules
- Never change runtime logic, control flow, or emitted JS to satisfy the types. If the code is genuinely wrong, report it separately — do not paper over it with types.
- Never loosen a type to silence an error (widening to
any, adding| undefinedto dodge a real null bug, making fields optional). - Never weaken
tsconfigor disable rules. - Keep the project compiling after every step: introduce no new type error, and if the baseline was green, never hand back a red build.