type-tightener
Skill0 starsChanged 6 months agoMITView on GitHub
Strip `any` (and its cousins `unknown`-without-narrowing, `as` casts, `@ts-ignore`) from a TypeScript codebase. Adds real types where they're missing, enables strict mode safely, and produces a migration plan rather than a Big Bang rewrite. Use when the user says "add types", "remove any", "enable strict mode", "fix the typescript", "tighten the types", or "we have too many `any`s".
In this repository16 other files it gives its agents.
- for-codex/AGENTS.mdAGENTS.md
- api-architectSkill
- browser-qaSkill
- code-auditorSkill
- doc-craftSkill
- git-flow-proSkill
- mcp-forgeSkill
- perf-hunterSkill
- refactor-masterSkill
- resilience-engineerSkill
- schema-architectSkill
- security-sentinelSkill
- ship-itSkill
- test-architectSkill
- threat-modelerSkill
- ui-polishSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
14 rules. About 0 are common, 3 are distinctive, and 11 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
11
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 18
Do *not* trigger for: type errors that are real runtime bugs (those need code changes, not type changes), or for "make this build green" …
- Distinctive: line 34
Before touching anything, count the debt:
- Very specific: line 63
Don't flip strict: true and stare at 4000 errors.
- Very specific: line 137
Every // @ts-ignore becomes either:
- Very specific: line 151
Add // @ts-expect-error regression tests for any narrowing you introduced:
- Very specific: line 170
Use satisfies to keep narrow inferred types while checking conformance to a wider one.
- Very specific: line 171
Prefer unknown over any when shape is unknown — the compiler forces narrowing.
- Very specific: line 172
Use Awaited<T>, ReturnType<T>, Parameters<T>, NonNullable<T> — they prevent type drift when the source changes.
- Very specific: line 175
Don't cast as SomeType over runtime data.
- Very specific: line 176
Don't use as unknown as X to bypass a real type mismatch.
- Very specific: line 177
Don't type primitives nominally (type UserId = string & { __brand: 'UserId' }) unless your team will actually use the branding.
- Distinctive: line 178
Don't type-only-fix. If the type was wrong because the runtime behavior was wrong, the fix is a code change first.
- Very specific: line 185
Check tsconfig: strict is on but noUncheckedIndexedAccess is off.
- Very specific: line 192
Verify: tsc --noEmit green, 412/412 tests pass, lint clean.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
type-tightener — types that catch bugs, not types that pretend to
When to use this skill
Trigger when the goal is type safety, not just "make TS shut up". Strong signals:
- "remove
anyfrom this file" - "enable
strictmode without breaking everything" - "we have 500 ts-ignore comments, help"
- "type this function properly"
- "the types are lying —
.useris sometimes undefined"
Do not trigger for: type errors that are real runtime bugs (those need code changes, not type changes), or for "make this build green" requests where the user wants you to suppress, not fix.
The output contract
Type changes that:
- Reduce the lie surface — fewer
anys, fewer unjustified casts, fewer suppressions - Compile —
tsc --noEmitgreen - Don't introduce false confidence — never replace
anywith a type that's wider than reality (e.g., typing JSON.parse output as a specific interface without runtime validation) - Land in safe slices — one file or one module at a time, never a Big Bang
- Document the residue — every remaining
anyor suppression has a comment explaining why
Workflow
1 — Inventory
Before touching anything, count the debt:
# count any (excluding type-fest, third-party d.ts)
rg -t ts ': any\b|<any>|as any' --no-heading -c | head
# count suppressions
rg -t ts '@ts-ignore|@ts-expect-error|@ts-nocheck' --no-heading -c | head
# count casts
rg -t ts ' as [A-Z]' --no-heading -c | headShow the numbers to the user. Decide together: pick a file to start, or take a sweep approach? Usually start with the highest-leverage module (auth, payments, the public API surface).
2 — Check the config
Look at tsconfig.json:
- Is
strict: trueon? If not, that's the long-term goal. - Specifically, the strict-mode flags that matter most:
strictNullChecks— without it, everyTis implicitlyT | null | undefined. Catches the most bugs.noImplicitAny— without it, missing types fall through silently.strictFunctionTypes— catches variance bugs in callbacks.strictPropertyInitialization— class fields must be initialized.
- Lint config: are
@typescript-eslint/no-explicit-anyand@typescript-eslint/no-non-null-assertionset to error or warn?
3 — Migration ladder
Don't flip strict: true and stare at 4000 errors. Climb in this order:
noImplicitAny— turn on first. Fix the resulting errors by adding types or explicit: unknown.strictNullChecks— biggest behavioral change. Fix one module at a time.strictFunctionTypes— usually small impact, low pain.strictPropertyInitialization— class-heavy codebases feel this most.noUncheckedIndexedAccess— paranoid mode for array/object access. Excellent for new code, painful for old.exactOptionalPropertyTypes— distinguishes{ x?: number }from{ x?: number | undefined }. Skip unless the project relies on the distinction.
For each flag, scope it per directory if needed (tsconfig.strict.json extends base, picks specific include paths) so green areas stay green while you work the brown ones.
4 — Replace any with intent
For each any, ask "what is this actually?" and pick:
Known shape — write the interface or type alias.
// before
function handle(req: any) { return req.body.user }
// after
interface SignupRequest { body: { user: { email: string; name: string } } }
function handle(req: SignupRequest) { return req.body.user }Unknown shape, parsed from outside (JSON, FormData, query string) — type as unknown, then validate with Zod / Valibot / io-ts.
// before
const data: any = await req.json()
const user = data.user as User
// after
const data: unknown = await req.json()
const result = SignupRequestSchema.safeParse(data)
if (!result.success) return res.status(422).json(result.error)
const user = result.data.user // ← real User type, validatedThis is the single biggest win in any TypeScript codebase. Replacing as casts at the boundary with real runtime validation.
Generic that lost its type — re-introduce the generic.
// before
function pick(obj: any, keys: string[]) { ... }
// after
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> { ... }Truly heterogeneous values (rare) — discriminated union.
type Event =
| { kind: 'click'; target: HTMLElement }
| { kind: 'submit'; form: HTMLFormElement }
| { kind: 'tick'; ms: number }5 — Replace casts
as X casts lie. Replace with:
- Narrowing — use a type guard.
function isUser(v: unknown): v is User { return typeof v === 'object' && v !== null && 'email' in v && typeof (v as any).email === 'string' } - Schema validation at the boundary (above).
- Real type inference — sometimes the cast is hiding a generic that the compiler could infer if you removed the cast.
The only as that's defensible: narrowing within a discriminated union (as const), or in tests where you're deliberately constructing partial values.
6 — Replace suppressions
Every // @ts-ignore becomes either:
- A real fix (most cases)
// @ts-expect-error <reason>— at least this fails if the underlying issue gets fixed- A documented escape with a TODO and an issue link
// @ts-nocheck at file top → split the file or fix incrementally; never leave it as a permanent solution.
7 — Verify
After the slice:
tsc --noEmitgreen- Existing tests still pass (you didn't change runtime behavior; tests confirm)
- Add
// @ts-expect-errorregression tests for any narrowing you introduced:// @ts-expect-error — must reject when email is missing SignupRequestSchema.parse({ user: { name: 'Ada' } })
8 — Report
Before: any-count 87, @ts-ignore 14, as-casts 32
After: any-count 19, @ts-ignore 0, as-casts 11
Remaining any: 19 (15 in src/legacy/, 4 in third-party glue with TODO comments)
Compile time delta: +0.3s (negligible)Patterns and anti-patterns
✅ Do:
- Type the boundaries (HTTP, DB, file, env) with schema validation. Trust the inside.
- Use
satisfiesto keep narrow inferred types while checking conformance to a wider one. - Prefer
unknownoveranywhen shape is unknown — the compiler forces narrowing. - Use
Awaited<T>,ReturnType<T>,Parameters<T>,NonNullable<T>— they prevent type drift when the source changes.
❌ Don't:
- Don't cast
as SomeTypeover runtime data. You're lying to the compiler and to yourself. - Don't use
as unknown as Xto bypass a real type mismatch. Find the actual incompatibility. - Don't type primitives nominally (
type UserId = string & { __brand: 'UserId' }) unless your team will actually use the branding. Otherwise it's friction with no payoff. - Don't type-only-fix. If the type was wrong because the runtime behavior was wrong, the fix is a code change first.
Example invocation
User: "remove every
anyfromsrc/api/."
- Inventory:
src/api/has 34 explicitany, 8@ts-ignore, 19ascasts. - Check tsconfig:
strictis on butnoUncheckedIndexedAccessis off; no@typescript-eslint/no-explicit-anyrule. Add the lint rule at warn level so newanys get flagged. - Triage the 34: 14 are unparsed request bodies, 10 are response types from a fetch wrapper, 6 are utility helpers that lost their generic, 4 are genuine "I don't know".
- Wave 1: add Zod schemas for the 14 request bodies in
schemas/. Replace bodies withawait schema.parseAsync(req.body). Inferred types flow through. (14anys gone) - Wave 2: introduce a typed
fetchJson<T>(url, schema): Promise<T>helper. Replace the 10 fetch-wrapper sites. (10anys gone) - Wave 3: restore generics on the 6 helpers. (6
anys gone) - Residue: 4
anys. Each gets a comment explaining the third-party SDK constraint and a TODO with an issue link. - Suppressions: 6 of 8
@ts-ignorebecome real fixes; 2 become@ts-expect-errorwith reasons. - Verify:
tsc --noEmitgreen, 412/412 tests pass, lint clean. - Report:
any34→4,@ts-ignore8→0,as19→11.
See also
code-auditor— catches the runtime bugs the types might be hidingtest-architect— add regression tests where new type-narrowing changed behaviorrefactor-master— when the typing exercise reveals a structural problem (e.g., a function doing two different things based on input shape)
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

