TypeScript norms: no `any`, no cast to silence the checker, strict flags on, and domain types that make an illegal state unrepresentable. Use when editing TypeScript files.
Installs into .claude/skills of the current project.
Are you the author of Refactor Typescript?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/univers42-refactor-typescript-claude-deal-with-the-devil)
---
name: refactor-typescript
description: >
TypeScript norms: no `any`, no cast to silence the checker, strict flags on, and domain
types that make an illegal state unrepresentable. Use when editing TypeScript files.
paths:
- "**/*.ts"
- "**/*.tsx"
user-invocable: false
metadata:
stage: rule
since: "1.0.0"
---
# TypeScript Refactoring
## Types are the point — don't opt out of them
- **No `any`.** Use `unknown` and narrow. An `any` that must stay carries an issue link
and a one-line reason, like any suppression (`rules/quality-bar.md`).
- No `as` to silence the compiler. A cast is a claim you are right and the checker is
wrong; prove it with a type guard instead. `as const` and `satisfies` are fine.
- No non-null `!`. If it cannot be null, the type should say so; if it can, handle it.
- `strict: true`, plus `noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`,
`noImplicitOverride`, `noFallthroughCasesInSwitch`. `arr[i]` is `T | undefined` and
that is correct.
- Model the domain so illegal states are unrepresentable: a discriminated union beats
four optional fields and a comment explaining which combinations are valid.
- Validate anything crossing the boundary at runtime — `zod`/`valibot` on API input,
env vars and parsed JSON. A TypeScript type is erased at runtime; it is documentation
the compiler checks, not a guard.
## Structure
- Max 40 lines per function; max 4 parameters, then an options object.
- Named exports only. A default export renames itself at every import site.
- No barrel `index.ts` re-exporting a whole tree — it breaks tree-shaking and creates
import cycles. Import from the module.
- One concern per file; a file over 300 lines is at least two modules.
- `readonly` on anything that is not mutated, arrays included (`readonly T[]`).
## Async
- Every promise is awaited or explicitly `void`-ed. A floating promise swallows its
rejection — `@typescript-eslint/no-floating-promises` catches it, keep it on.
- `Promise.all` for independent work; sequential `await` in a loop is a serial
bottleneck unless order matters. `Promise.allSettled` when one failure must not
cancel the rest.
- No `async` on a function that never awaits.
- Errors in `catch` are `unknown`. Narrow before reading `.message`.
## React and the DOM (`.tsx`)
- The dependency array is complete. Disabling `react-hooks/exhaustive-deps` is a bug
report, not a fix.
- Derive during render instead of syncing with an effect. Most `useEffect` that sets
state is a computed value in disguise.
- Every effect that subscribes, times or fetches returns its cleanup.
- Stable keys from data identity, never the array index.
- Accessibility is part of correctness, not polish: semantic elements before
`role`, a label on every input, focus managed on route and dialog changes, and
contrast that passes (`rules/quality-bar.md` layer 6, and the `frontend` skill).
## Dependencies
- Check `package.json` before adding anything — the project probably already has a
date library, a fetch wrapper and a validator (`rules/minimalism-ladder.md` rung 4).
- `date-fns`/`Temporal` over moment; native `fetch` over axios in a modern runtime;
`structuredClone` over a deep-clone dependency.
- A dependency used for one function gets inlined.
## After refactoring
- `tsc --noEmit` — zero errors.
- `eslint . --max-warnings 0` with `@typescript-eslint` strict-type-checked, plus
`jsx-a11y` for anything rendered.
- `prettier --check .`
- The detected test runner green (`rules/test-frameworks.md` — Vitest for new projects,
Jest where it already is).
- `npm audit --audit-level=high` / `osv-scanner` — no known-vuln dependency.