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.

install
source · Clone the upstream repo
git clone https://github.com/raintree-technology/claude-starter
Claude Code · Install into ~/.claude/skills/
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"
manifest: skills/code-quality/cleanup-weak-types/SKILL.md
source content

Replace 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

  1. Language detect: TS/JS (
    any
    ,
    unknown
    ,
    as unknown as
    ,
    Function
    ,
    Object
    ), Python (
    Any
    , missing type hints), Go (
    interface{}
    ,
    any
    since 1.18), Rust (
    Box<dyn Any>
    is rare; mostly look for
    Box<dyn Trait>
    where a concrete type would do).
  2. Git state: refuse on dirty tree.
  3. Report dir: ensure exists.
  4. Read project conventions: check for a
    check:weak-types
    or similar script in
    package.json
    . Check
    tsconfig.json
    for
    strict
    /
    noImplicitAny
    flags. Check
    mypy.ini
    /
    pyproject.toml [tool.mypy]
    for strictness.
  5. Read allow-list: many projects allow weak types in specific files (e.g.,
    *.test.ts
    , generated code, third-party shim files). Find and respect them.

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
    .d.ts
    , part of an SDK).
  • Generic-amenable signatures (suggest the generic but don't apply).
  • Discriminated union opportunities — the human picks the discriminator field.
  • as unknown as
    casts — these usually indicate a deeper type design problem.

LOW (note, no action):

  • Boundary code receiving genuine unknown input (HTTP body before validation,
    JSON.parse
    , dynamic config).
  • 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:

  1. Capture the exact
    git diff
    of the proposed change.
  2. Apply the change (Edit).
  3. Run scoped typecheck:
    npm run typecheck
    or
    npx tsc --noEmit
    . For Python:
    mypy <file>
    .
  4. If typecheck fails OR introduces new errors elsewhere:
    git checkout -- <file>
    , downgrade this finding to MEDIUM in the report, continue.
  5. 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
    any
    with
    unknown
    — that's a different defect, not a fix. Both are weak;
    unknown
    just forces narrowing.
  • Replace
    any
    with an over-narrow type that breaks one of N callers — verify ALL callers fit.
  • Touch generated types (Drizzle
    $inferSelect
    , OpenAPI codegen, Prisma) — fix the codegen config instead.
  • Add
    // @ts-ignore
    or
    # type: ignore
    to make the change pass — that's hiding the problem.
  • Modify ambient
    .d.ts
    declarations for third-party libraries.
  • Remove an
    as unknown as
    cast without understanding why it was added — it's often masking a type incompatibility worth investigating, not silently fixing.
  • Auto-add generics to public APIs — that's a contract change requiring human design.