Claude-starter cleanup-weak-types
Replace weak types (any, unknown, interface{}, untyped Python) with strong, inferable types. Researches actual usage to determine the correct type, runs typecheck after each change, reverts individual changes that fail. Use when the user asks to remove any/unknown, strengthen typing, fix weak types, or make code more type-safe.
git clone https://github.com/raintree-technology/claude-starter
T=$(mktemp -d) && git clone --depth=1 https://github.com/raintree-technology/claude-starter "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/code-quality/cleanup-weak-types" ~/.claude/skills/raintree-technology-claude-starter-cleanup-weak-types && rm -rf "$T"
skills/code-quality/cleanup-weak-types/SKILL.mdReplace weak escape-hatch types with strong types inferred from actual usage. Per-occurrence verification — each replacement is typechecked individually, reverted if it breaks. Conservative on public APIs.
Preflight
- Language detect: TS/JS (
,any
,unknown
,as unknown as
,Function
), Python (Object
, missing type hints), Go (Any
,interface{}
since 1.18), Rust (any
is rare; mostly look forBox<dyn Any>
where a concrete type would do).Box<dyn Trait> - Git state: refuse on dirty tree.
- Report dir: ensure exists.
- Read project conventions: check for a
or similar script incheck:weak-types
. Checkpackage.json
fortsconfig.json
/strict
flags. ChecknoImplicitAny
/mypy.ini
for strictness.pyproject.toml [tool.mypy] - Read allow-list: many projects allow weak types in specific files (e.g.,
, generated code, third-party shim files). Find and respect them.*.test.ts
Detect
TypeScript / JavaScript
# Explicit `any` grep -rn --include="*.ts" --include="*.tsx" -E "\b(: any\b|<any>|as any\b|as unknown as)" \ --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=.next . > /tmp/ts-weak.txt # Compiler-derived implicit-any (more accurate than grep) npx tsc --noImplicitAny --noEmit 2>&1 | grep "implicitly has an 'any' type" > /tmp/ts-implicit-any.txt
For each occurrence, capture the surrounding context (function signature, callers).
Python
# Explicit Any imports + usage grep -rn --include="*.py" -E "(from typing import.*Any|: Any\b|-> Any\b)" . > /tmp/py-any.txt # Mypy strict mode finds untyped functions mypy --disallow-untyped-defs --no-incremental . > /tmp/py-untyped.txt 2>&1 || true
Go
grep -rn --include="*.go" -E "\binterface\{\}|\bany\b" . > /tmp/go-any.txt
Rust
grep -rn --include="*.rs" -E "(Box<dyn |&dyn )" . > /tmp/rust-dyn.txt
Assess
Write
.claude/cleanup-reports/cleanup-weak-types-{YYYY-MM-DD}.md:
# Weak Types Assessment — YYYY-MM-DD ## Summary - Total weak-type sites: N - HIGH (safe to auto-fix): X - MEDIUM (public API or cross-package): Y - LOW (justified — e.g., genuine unknown JSON, third-party): Z ## Findings ### HIGH — `apps/app/lib/parse.ts:45` `function process(data: any)` - Inferable type: `data` is always called with `{ id: string; events: Event[] }` (3 callers checked). - Replacement: `function process(data: { id: string; events: Event[] })`. - Even better: lift to a named type `ProcessInput`. ### MEDIUM — `packages/sdk/src/client.ts:12` `function send(payload: any): Promise<any>` - Public API of an SDK package — changing the type is a breaking change. - Recommendation: introduce a generic `<T, R>` and have callers specify, OR use `unknown` and require validation. ### LOW — `lib/json.ts:8` `function parseJson(s: string): unknown` - Genuinely unknown — JSON.parse output. Keep as `unknown`, ensure callers narrow. ## Critical Assessment [2-3 paragraphs: where are weak types concentrated? Boundary code (HTTP handlers, JSON parsing) often justifies them. Internal logic almost never does.]
Apply
Auto-fix HIGH only, ONE AT A TIME with typecheck between each. This is essential — bulk type changes can cascade in hard-to-predict ways.
Confidence rubric
HIGH (auto-apply, individually):
- The weak type is in a private/internal function.
- All callers are in the same repo and pass the same type (or a small finite set easily expressed as a union).
- Replacement is mechanically derivable from usage.
- No re-export of the symbol from a package boundary.
MEDIUM (report only):
- Public API surface (exported from a package, used by a
, part of an SDK)..d.ts - Generic-amenable signatures (suggest the generic but don't apply).
- Discriminated union opportunities — the human picks the discriminator field.
casts — these usually indicate a deeper type design problem.as unknown as
LOW (note, no action):
- Boundary code receiving genuine unknown input (HTTP body before validation,
, dynamic config).JSON.parse - Third-party shim files where the actual library is untyped.
- Test files (allowed in most weak-type allow-lists).
Execution (HIGH, individually)
For EACH HIGH finding:
- Capture the exact
of the proposed change.git diff - Apply the change (Edit).
- Run scoped typecheck:
ornpm run typecheck
. For Python:npx tsc --noEmit
.mypy <file> - If typecheck fails OR introduces new errors elsewhere:
, downgrade this finding to MEDIUM in the report, continue.git checkout -- <file> - If typecheck passes, move on.
After all HIGH findings processed, single commit:
chore(cleanup): cleanup-weak-types — strengthened N type signatures.
Verify
# Full typecheck across the repo (not just changed files) npm run typecheck 2>&1 || npx tsc --noEmit mypy --strict . 2>&1 || mypy . 2>&1 go build ./... 2>&1 cargo check 2>&1 # Tests — important here, since type changes can affect runtime via narrowing npm test 2>&1 pytest 2>&1 go test ./... 2>&1 cargo test 2>&1 # Project-specific weak-types gate (if the project defines one) npm run check:weak-types 2>/dev/null || true
If anything fails after the per-file pass somehow (rare but possible across-file inference): revert all and downgrade. Should rarely happen because we typecheck after each.
Output
- "Strengthened N weak types. M deferred for review."
- Report path with breakdown of HIGH/MEDIUM/LOW.
- Verify status.
NEVER
- Bulk replace
withany
— that's a different defect, not a fix. Both are weak;unknown
just forces narrowing.unknown - Replace
with an over-narrow type that breaks one of N callers — verify ALL callers fit.any - Touch generated types (Drizzle
, OpenAPI codegen, Prisma) — fix the codegen config instead.$inferSelect - Add
or// @ts-ignore
to make the change pass — that's hiding the problem.# type: ignore - Modify ambient
declarations for third-party libraries..d.ts - Remove an
cast without understanding why it was added — it's often masking a type incompatibility worth investigating, not silently fixing.as unknown as - Auto-add generics to public APIs — that's a contract change requiring human design.