fix-typescript · diff
v1.0.0 to v1.1.0
26 added, 95 removed. Audit A to A.
---
name: fix-typescript
description: "Fix TypeScript errors with strict anti-reward-hacking rules. **ALWAYS use when** the user says 'fix type errors', 'fix typescript', 'type-check is failing', or when TypeScript compilation errors need to be resolved. Ensures runtime type safety — fixes root causes instead of silencing errors with casts."
disable-model-invocation: false
allowed-tools: Read, Edit, Bash, Grep
- version: 1.0.0
+ version: 1.1.0
---
# Fix TypeScript Errors
- You are fixing TypeScript type errors. This command embeds strict rules to prevent "reward hacking" - patterns that make linters pass without actually fixing type safety issues.
-
- ## CRITICAL: Read Before Starting
-
- **Your goal is RUNTIME TYPE SAFETY, not just passing linters.**
+ Fix TypeScript type errors for **runtime type safety**, not just to satisfy the linter. If a fix would pass `tsc` but could still crash at runtime, it's wrong.
- If your fix would make the linter happy but could still crash at runtime, it's wrong.
+ ## Forbidden patterns
- ## Forbidden Patterns
+ The canonical, exhaustive forbidden-pattern table — `as any`, `as unknown as`, `@ts-ignore` / `@ts-expect-error`, void tricks, underscore-prefixed unused locals, non-null assertions without a guard, `forEach(async`, and exported unused types, each with severity and acceptable/unacceptable examples — lives in `/catalyst-dev:scan-reward-hacking` (`plugins/dev/skills/scan-reward-hacking/SKILL.md`). Run it before marking this work complete; don't re-derive the list here.
- These patterns are EXPLICITLY FORBIDDEN. Using them will require rework:
+ Two more rules are process-level, not grep-able code patterns, so `/catalyst-dev:scan-reward-hacking` doesn't scan for them — they are still forbidden:
- | Pattern | Why Forbidden | What To Do Instead |
- | ----------------------------------------- | ---------------------------------------- | ------------------------------------- |
- | `as unknown as Type` (undocumented) | Erases all type info, defeats TypeScript | Fix the source to return correct type |
- | `as any` | Disables type checking entirely | Use proper typing or Zod validation |
- | `void (0 as unknown as Type)` | Tricks linter into thinking type is used | Delete the unused type |
- | `const _var = ...` (local variable) | Suppresses unused warning | Delete the unused variable |
- | `export type Foo` (when unused elsewhere) | Suppresses unused warning | Remove `export` or delete the type |
- | `// @ts-ignore` or `// @ts-expect-error` | Hides real type problems | Fix the actual type error |
- | Commenting out code | Dead code clutters codebase | Delete it (git has history) |
- | Excluding files from tsconfig | Hides errors in those files | Include files, fix errors |
+ - **Commenting out code** instead of deleting it (git has history).
+ - **Excluding files from `tsconfig`** to hide errors instead of fixing them.
- ## Required Approach
+ ## Stricter than the canonical scan
- ### For Internal Code (services, models, aggregations)
+ Three cases the canonical scan accepts are not acceptable here, because you are actively fixing the error rather than auditing pre-existing code: an exported-but-unused type must be removed or unexported outright, not left as the canonical scan's informational note; `@ts-ignore` / `@ts-expect-error` are never acceptable, even with a documented reason and tracking ticket — fix the error instead of suppressing it; and a test mock's `as any` is not a license to introduce a new one while you're resolving a production type error.
- Fix the SOURCE to return the correct type:
+ ## Fix at the source, not the consumer
```typescript
- // WRONG - Casting a query result
+ // WRONG - cast a query result
const users = (await db.from("users").select("*")) as unknown as User[];
- // CORRECT - Use the query builder's generic typing
+ // CORRECT - use the query builder's generic typing
const { data: users } = await db.from("users").select("*").returns<User[]>();
```
- ```typescript
- // WRONG - Casting a service result
- const account = (await accountService.findById(id)) as Account;
-
- // CORRECT - Fix the service to return the right type
- const account = await accountService.findById(id); // Returns Account | null
- if (account === null) throw new NotFoundError();
- ```
-
- ### For External Data (API requests, webhooks, file uploads)
-
- Validate with Zod at the boundary:
+ For **external data** (API requests, webhooks, uploads), validate with Zod at the boundary instead of casting:
```typescript
- // WRONG - Trust external data
+ // WRONG
const webhook = req.body as WebhookPayload;
-
- // CORRECT - Validate at boundary
+ // CORRECT
const webhook = WebhookPayloadSchema.parse(req.body);
```
- ## When Type Assertions ARE Acceptable
+ ## When a type assertion is acceptable
- Type assertions are ONLY acceptable for **third-party library limitations** with full documentation:
+ Only for a **third-party library limitation**, documented with what was verified at runtime and a tracking ticket:
```typescript
- // ACCEPTABLE - Third-party library type gap with documentation
- // LIBRARY TYPE LIMITATION: The thirdPartyWrapper() function returns a type
- // that TypeScript can't verify implements the expected interface.
- // Verified at runtime that the object has the required methods.
- // TODO: Remove when library updates types (tracked in TICKET-XXX)
+ // LIBRARY TYPE LIMITATION: thirdPartyWrapper() returns a type TS can't verify.
+ // Verified at runtime it has the required methods. TODO: remove when the library
+ // updates types (tracked in TICKET-XXX).
const wrapped = thirdPartyResult as unknown as ExpectedInterface;
```
- **Requirements for acceptable assertions:**
-
- 1. The type error is from a THIRD-PARTY LIBRARY we don't control
- 2. There's a detailed comment explaining WHY
- 3. The comment explains what was verified at runtime
- 4. There's a TODO with a tracking ticket
-
## Process
- 1. **Understand the error**: Read the TypeScript error message carefully
- 2. **Find the root cause**: Why doesn't the type already match?
- 3. **Fix at the source**: Change the source function/type, not the consumer
- 4. **Verify the fix**: Detect the package manager and run type-check (see below)
- 5. **Run validation**: Execute `/scan-reward-hacking` before marking complete
-
- ## Detecting Package Manager and Type-Check Script
-
- First, check `package.json` for a `type-check` script. Then detect the package manager by looking for lock files:
-
- - `bun.lockb` -> `bun run type-check`
- - `pnpm-lock.yaml` -> `pnpm type-check`
- - `yarn.lock` -> `yarn type-check`
- - `package-lock.json` -> `npm run type-check`
-
- If no lock file is found, fall back to `npx tsc --noEmit`.
-
- ## Before Marking Complete
-
- You MUST run these checks:
-
- ```bash
- # Type check must pass (use the detected package manager)
- # e.g., npm run type-check / yarn type-check / bun run type-check / pnpm type-check
-
- # Scan for forbidden patterns (run /scan-reward-hacking command)
- grep -r "as unknown as" [files-you-changed]
- grep -r "void (0" [files-you-changed]
- grep -r "as any" [files-you-changed]
- ```
-
- If ANY forbidden pattern exists in your changes without proper documentation, your work is not complete.
-
- ## The Golden Rule
-
- **If you need `as`, ask "Why doesn't the type already match?" and fix THAT.**
-
- Type assertions are a code smell. They mean either:
+ 1. Read the TypeScript error; find the root cause — why doesn't the type already match?
+ 2. Fix at the source (the producing function/type), not the consumer.
+ 3. Detect the package manager (`bun.lockb` / `pnpm-lock.yaml` / `yarn.lock` / `package-lock.json`, else `npx tsc --noEmit`) and run its `type-check` script.
+ 4. Run `/catalyst-dev:scan-reward-hacking` on the files you changed. Work is not complete until it passes.
- 1. The source function has wrong types (fix the source)
- 2. External data wasn't validated (add Zod validation)
- 3. A library has incomplete types (document and track)
+ ## The golden rule
- Never use assertions to silence errors - fix the underlying type mismatch.
+ If you reach for `as`, ask "why doesn't the type already match?" and fix that instead. A type assertion means the source has wrong types (fix the source), external data wasn't validated (add Zod), or a library has incomplete types (document and track) — never use it to silence an error.