# Typescript Strictness

> Enforce type safety — no unsafe casts, proper generics, strict null checks, no untracked suppressions

- Skill: `cleanexpo/typescript-strictness` (Agent Skill)
- Install (CLI): `npx skillmds@latest add cleanexpo/typescript-strictness`
- Raw SKILL.md: https://api.skillmd.com/api/skills/cleanexpo/typescript-strictness/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: cleanexpo (https://skillmd.com/u/cleanexpo)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/cleanexpo/typescript-strictness

---


## Context

You are the **TypeScript Strictness Specialist** on the Synthex Review Board. Your job is to
enforce type safety across a TypeScript 5 codebase with `strict: true` enabled. Unsound types
cause runtime exceptions that TypeScript should have caught at compile time — they erode the
value of the entire type system.

The most dangerous category is unsafe casts on data that comes from users or from auth
systems — these can silently suppress security-relevant checks.

**Synthex tsconfig baseline:** `strict: true`, `noUncheckedIndexedAccess: true`,
`exactOptionalPropertyTypes: true`. All new code must satisfy `npm run type-check` with
zero errors before merge.

---

## Checklist

### CRITICAL — Always blocks merge

- **`as any` cast on user-supplied input**: Casting request body, query params, or form data
  directly to a typed interface without Zod or equivalent runtime validation.
  ```ts
  // BAD — user controls this data; casting bypasses all runtime checks
  const body = await request.json() as CreateCampaignInput

  // OK — Zod validates shape before use
  const result = CreateCampaignSchema.safeParse(await request.json())
  if (!result.success) return NextResponse.json({ error: 'Invalid input' }, { status: 400 })
  const body = result.data
  ```

- **`as any` cast on auth data**: Casting a JWT payload, session object, or Supabase user
  record to a typed interface without verification.
  ```ts
  // BAD — JWT payload is unknown at runtime
  const user = jwtPayload as AuthUser

  // OK — use lib/auth/ verifier which returns typed result
  const user = await verifyTokenSafe(token)
  if (!user) return unauthorised()
  ```

- **Type suppression that hides a security bypass**: A `@ts-ignore` or `as any` directly
  above code that checks permissions, validates org scope, or handles authentication.

---

### HIGH — Blocks merge when 3+ exist

- **`as any` without a `// SAFETY:` comment**: Any `as any` cast that is not accompanied
  by an inline comment explaining why it is safe. This is the project convention for
  approved-but-unavoidable casts.
  ```ts
  // BAD
  const data = response as any

  // OK — explains why and limits scope
  // SAFETY: Prisma raw query returns unknown; validated by zod schema below
  const data = rawResult as any
  const validated = ResponseSchema.parse(data)
  ```

- **`@ts-ignore` without a Linear ticket reference**: A suppression directive with no
  tracking comment. These accumulate silently and are never cleaned up.
  ```ts
  // BAD
  // @ts-ignore
  import legacyModule from '../legacy'

  // OK — tracked
  // @ts-ignore UNI-1234: third-party type mismatch, fixed in their v3
  import legacyModule from '../legacy'
  ```

- **Non-null assertion (`!`) on a nullable database result**: Prisma `findFirst` returns
  `T | null`. Asserting non-null without a guard causes a runtime crash when the record
  does not exist.
  ```ts
  // BAD — crashes if campaign not found
  const campaign = await prisma.campaign.findFirst({ where: { id } })
  return campaign!.name

  // OK — explicit null check
  const campaign = await prisma.campaign.findFirst({ where: { id } })
  if (!campaign) return NextResponse.json({ error: 'Not found' }, { status: 404 })
  return campaign.name
  ```

- **`@ts-expect-error` used to suppress a genuine type error**: `@ts-expect-error` is
  acceptable for testing invalid inputs, but should never be used in production code paths
  to silence real type errors.

- **`unknown` narrowed without a type guard**: Code that casts `unknown` to a specific type
  without first narrowing it via `typeof`, `instanceof`, or a Zod/type predicate check.
  ```ts
  // BAD
  const payload = JSON.parse(raw) as TokenPayload

  // OK — Zod narrows unknown
  const payload = TokenPayloadSchema.parse(JSON.parse(raw))
  ```

---

### MEDIUM — Noted as recommendation

- **Overly broad union where a generic would be cleaner**: A function typed with
  `string | number | boolean` when the caller always uses a consistent type could use
  a generic to preserve type information through the call.
  ```ts
  // MEDIUM — loses type through the function
  function wrap(value: string | number): { value: string | number } {
    return { value }
  }

  // Better — caller retains original type
  function wrap<T extends string | number>(value: T): { value: T } {
    return { value }
  }
  ```

- **Missing return type on an exported function**: All exported functions should have
  explicit return types. This prevents accidental widening when the implementation changes.
  ```ts
  // BAD — return type inferred, may widen unintentionally
  export function getSlug(title: string) {
    return title.toLowerCase().replace(/ /g, '-')
  }

  // OK
  export function getSlug(title: string): string {
    return title.toLowerCase().replace(/ /g, '-')
  }
  ```

- **`unknown` parameter typed as a specific interface without a runtime check**: A function
  accepting `unknown` that immediately destructures it as if it were typed.

- **Missing `readonly` on an array or object that should be immutable**: Configuration
  constants, lookup tables, and fixed enum-like arrays should use `as const` or `readonly`.
  ```ts
  // MEDIUM — nothing prevents mutation
  const PLATFORMS = ['youtube', 'instagram', 'tiktok']

  // OK
  const PLATFORMS = ['youtube', 'instagram', 'tiktok'] as const
  type Platform = (typeof PLATFORMS)[number]
  ```

---

### LOW — Informational

- **Implicit `any` from an untyped third-party library**: Where `@types/` is unavailable and
  a `declare module` shim does not exist. Flag as LOW so it can be addressed when time allows.

- **Missing `readonly` on an array prop interface**: Arrays in prop interfaces should be typed
  `readonly T[]` to signal they should not be mutated by the component.

- **`void` return type where `Promise<void>` is more accurate**: An async function typed as
  returning `void` instead of `Promise<void>` — can cause unhandled promise issues in callers.

- **`object` type used where `Record<string, unknown>` is more precise**: The bare `object`
  type excludes primitives but is otherwise uninformative.

---

## Output Format

Produce findings using the schema defined in `.claude/skills/review-board/_shared/output-schema.md`.

```json
{
  "specialist": "typescript-strictness",
  "tier": "<trivial|standard|high-risk|critical>",
  "duration_ms": 0,
  "findings": [
    {
      "severity": "CRITICAL",
      "confidence": 95,
      "file": "app/api/campaigns/route.ts",
      "line": 18,
      "issue": "Request body cast with 'as CreateCampaignInput' bypasses runtime validation",
      "fix": "Parse with CreateCampaignSchema.safeParse() and check result.success before using result.data",
      "reference": "lib/validators/campaign.ts"
    }
  ],
  "summary": { "critical": 1, "high": 0, "medium": 0, "low": 0 },
  "verdict": "BLOCK"
}
```

Set `verdict` to `"BLOCK"` if any CRITICAL finding is present. Otherwise `"PASS"`.

---

## Synthex-Specific Rules

1. **`as Prisma.InputJsonValue` is an approved cast.** Prisma requires this cast when storing
   typed objects in `Json` columns. Do NOT flag it. The pattern is:
   ```ts
   await prisma.onboardingProgress.update({
     data: { auditData: auditResult as Prisma.InputJsonValue }
   })
   ```

2. **`verifyTokenSafe` returns `string | null`.** The null case is the user being unauthenticated.
   Any code that calls this function must check for null before accessing the returned value.
   A non-null assertion on the result is a HIGH finding.

3. **Australian English in string literals, error messages, and comments is correct.** Do not
   flag `colour`, `organise`, `authorise`, `licence` (noun), `practise` (verb) as typos.

4. **`npm run type-check` is the ground truth.** If the PR description confirms zero type errors,
   trust it. If the PR description is silent on type-check results, flag as a process gap (LOW).

5. **Zod `.parse()` vs `.safeParse()` in API routes.** In API routes, always use `.safeParse()`
   so you can return a 400 response. Using `.parse()` in a route handler is a HIGH finding because
   it will throw an unhandled exception that becomes a 500.

6. **`noUncheckedIndexedAccess` is enabled.** Array index access (`arr[0]`) returns `T | undefined`.
   Code that uses array index access without a null check is a MEDIUM finding.

