---
name: unsafe-type-escape-review
source: https://app.decimal.ai/s/unsafe-type-escape-review@1/SKILL.md
source_sha256: 93e94358a6ff
---

# Unsafe Type-Escape Review

TypeScript's checker only helps where you let it. A handful of constructs turn it off locally — and by default a reviewer waves them through ("compiles, looks fine") because the code *does* compile: that is exactly what the escape hatch bought. The bug it was hiding surfaces at runtime instead. This skill makes the review flag each escape hatch and name the concrete type-safe replacement, so the fix is actionable rather than a vague "add types".

## When to activate

Activate when the request hands you **TypeScript code to review, tighten, or make type-safe** — a diff, a file, a PR, a snippet — especially where the code reaches for one of the constructs below.

Do **not** activate for tsconfig / build configuration, for a language that isn't TypeScript, for explaining a type-system concept in the abstract, or for making a real compile error go away (suppressing an error is the anti-pattern this review flags, not the goal).

## The escape hatches

Each row: what the construct silences, and the type-safe replacement to recommend in its place.

| Construct | What it silences | Type-safe replacement |
|---|---|---|
| `any` (annotation, `any[]`, `Record<string, any>`) | *all* checking on that value — and `any` spreads silently to everything it touches | `unknown` + narrowing before use, or a precise type / generic |
| `as` assertion (`x as T`) | the compiler's own inference — no runtime check happens | validate into the type (a type guard or a schema parse), or fix the source type / use a generic |
| non-null `!` (`x!`, `x!.foo`) | the "possibly `null`/`undefined`" error, without proving it isn't | an explicit check that narrows (`if (x) …`), or make the type honest |
| `@ts-ignore` | the *next line's* error forever, even after the code changes | fix the underlying type; if a suppression is truly unavoidable, use `@ts-expect-error` **with a reason** — it fails the build once the error is gone |
| bare `object` / `Function` / `{}` | the actual shape — `object` = any non-primitive, `Function` returns `any` on call, `{}` = any non-null | a specific interface / `Record<string, V>`; a real call signature `(a: A) => R`; a named shape instead of `{}` |
| unchecked index access (`arr[i]`, `map[key]`) | that the element may be missing — the type claims it's always present | enable `noUncheckedIndexedAccess` so the result is `T | undefined`, then guard; or `.at()` / `.get()` / a presence check |

### Why each replacement is safer

- **`any` → `unknown`.** Both accept anything, but `unknown` forbids *using* the value until you narrow it, so the check moves to where the data actually enters. `any` leaks: one `any` return type quietly untype-checks every caller downstream.
- **`as` → validate.** `data as ApiResponse` asserts a shape the compiler never verified; if the payload differs, the mismatch is a runtime crash at first use. A type guard (`function isApiResponse(v: unknown): v is ApiResponse`) or a parse turns the claim into a checked fact. Watch for the double-assertion `x as unknown as T` — that is a deliberate detour around the compiler's refusal.
- **`!` → narrow.** `!` is a promise to the compiler with no evidence. Replace with the check that supplies the evidence, or, if the value genuinely can't be absent, fix the type that says it can.
- **`@ts-ignore` → `@ts-expect-error`.** `@ts-ignore` silently keeps suppressing even after the underlying code is fixed, so dead suppressions accumulate. `@ts-expect-error` errors if the line stops having an error — it self-cleans. Neither is a substitute for fixing the type when you can.
- **bare `object`/`Function` → a shape.** These accept far more than intended. `Function` is the worst: calling it yields `any`, re-opening the hole. Name the real signature or interface.
- **index access → `T | undefined`.** By default `const row = rows[i]` is typed as present even when `i` is out of range, so `row.name` type-checks and then throws at runtime. `noUncheckedIndexedAccess` makes the optionality visible so you must handle it.

## How to review

1. Scan for each construct above. For every occurrence, state which hatch it is and what it hides.
2. Name the specific replacement (not "add a type" — say *which*: `unknown` + narrow, a type guard, a generic, a precise shape, an index guard).
3. Distinguish a genuine unavoidable case (a truly dynamic boundary, a third-party type gap) from a lazy one — the former still deserves the *narrowest* hatch and a comment; the latter should just be fixed.
4. If the code is already type-safe, say so and name what it did right — don't invent a problem.

## Output

- **Findings:** one per escape hatch — the construct, where, what it hides, and the concrete replacement.
- **Verdict:** *type-safe* / *N escape hatch(es) to replace before merge*.

## Example

**Reviewing:**

```ts
function handle(payload: any) {
  const user = payload as SessionUser;
  return cache.get(user.id)!.token;
}
```

**Review:**

- `payload: any` — disables checking on `payload` and everything derived from it. Type the parameter `unknown` and narrow, or give it the precise inbound type.
- `payload as SessionUser` — an unverified assertion; if the payload isn't a `SessionUser` this is a runtime crash at `user.id`. Validate with a type guard or a schema parse instead of asserting.
- `cache.get(user.id)!` — the non-null `!` hides that `.get()` can return `undefined` on a miss. Check the result (`const entry = cache.get(user.id); if (!entry) …`) before reading `.token`.

**Verdict:** 3 escape hatches to replace before merge.

## Edge cases

- **Justified hatch at a real boundary** (e.g. `JSON.parse` returns `any` inherently): accept it, but narrow immediately — assign to `unknown` and validate, don't let the `any` propagate.
- **Test fixtures using `as` for partial data:** still a hatch; prefer a typed partial-builder over `as` so the fixture stays honest as the type evolves.
- **`@ts-expect-error` with a reason:** already the good form — don't flag it as if it were `@ts-ignore`.
- **`unknown` already in use:** that's the fix, not a finding — confirm it's narrowed before use rather than re-asserted with `as`.
- **Under-scope:** this reviews type-safety escape hatches only; it is not a full logic or security review.
