Skip to content

chore(ts): enable noUncheckedIndexedAccess + fix all 323 errors#208

Merged
srsholmes merged 1 commit into
claude/ts-hardeningfrom
claude/ts-strict-indexed
Jul 8, 2026
Merged

chore(ts): enable noUncheckedIndexedAccess + fix all 323 errors#208
srsholmes merged 1 commit into
claude/ts-hardeningfrom
claude/ts-strict-indexed

Conversation

@srsholmes

Copy link
Copy Markdown
Owner

Stacked on #207 (base = claude/ts-hardening), so this diff shows only the strictness work. Retarget to main once #207 merges.

What

Enables noUncheckedIndexedAccess — the highest-value deferred flag from #207. Indexed access (arr[i], obj[key], regex groups, .split() parts, array destructuring) now yields T | undefined, catching a large class of latent "possibly undefined" bugs.

Fixed all 323 resulting errors across ~90 files, via 8 parallel fixers over disjoint areas, then reconciled. Every fix is proper — no suppressions:

  • Guards / early-returns / continue where a value can genuinely be missing (parser rows, device lookups, empty collections).
  • ?? fallback where a sensible default exists.
  • Non-null ! only where provably in-bounds — after a .length check, inside 0..len-1 loops, on required regex capture groups, or on findIndex results — each with a short justifying comment.

Guardrails verified

  • No any / as any / as unknown as / @ts-ignore / @ts-expect-error introduced (audited the diff). The one pre-existing React-fiber as unknown as in qam.ts was preserved unchanged (the agent just hoisted divs[i] into a guarded local).
  • Runtime behavior unchanged — guards only fire on inputs that would previously have thrown or gone undefined downstream. The parser test suites (vdf, storage-cleaner, network, input-plumber, etc.) still pass.

Checks

tsgo 0 · tsc 0 · eslint 0 errors · 364 tests pass.

Still deferred

exactOptionalPropertyTypes (~89 errors) — next follow-up. noPropertyAccessFromIndexSignature (~261, low value) — skipping.

🤖 Generated with Claude Code

Turn on `noUncheckedIndexedAccess` — indexed access (arr[i], obj[key],
regex groups, .split() parts, array destructuring) now yields `T | undefined`,
catching a large class of latent "possibly undefined" bugs.

Fixed all 323 resulting errors across ~90 files, properly (no suppressions):
- Real guards / early-returns / `continue` where a value can genuinely be
  missing (parser rows, device lookups, empty collections).
- `?? fallback` where a sensible default exists.
- Non-null `!` ONLY where provably in-bounds — after a `.length` check, inside
  `0..len-1` loops, on required regex capture groups, or on `findIndex`
  results — each annotated with a short justifying comment.

No `any` / `as any` / `as unknown as` / `@ts-ignore` introduced; the one
pre-existing React-fiber `as unknown as` in qam.ts was preserved as-is.
Runtime behavior unchanged (guards only fire on inputs that would previously
have thrown or produced undefined downstream); parser test suites still pass.

tsgo: 0 · tsc: 0 · eslint: 0 errors · 364 tests pass.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@srsholmes srsholmes merged commit 2508350 into claude/ts-hardening Jul 8, 2026
2 checks passed
@srsholmes srsholmes deleted the claude/ts-strict-indexed branch July 8, 2026 16:22
srsholmes added a commit that referenced this pull request Jul 9, 2026
…heckedIndexedAccess (#207)

* chore(ts): harden TypeScript checking + add Go-native tsgo gate in CI

Pragmatic TS hardening (deps untouched):

- tsconfig: enable noImplicitReturns + noUnusedParameters (both strict,
  near-zero churn). Skipped the high-churn/low-value flags for now
  (noPropertyAccessFromIndexSignature ~261 errs); noUncheckedIndexedAccess
  (~234) and exactOptionalPropertyTypes (~89) are left for follow-up PRs.
- Broaden `include` to cover plugins' lib/ and components/ (+ top-level
  plugin files) — previously only backend.ts/app.tsx/panel.tsx entry files
  were checked, so lib code was only validated transitively. Add an explicit
  `exclude` for test/spec, build, and .cache so this stays production-focused.
- Adopt the Go-native TypeScript preview (@typescript/native-preview / tsgo),
  pinned exactly, as a second CI typecheck step. It's ~9x faster than tsc and
  slightly stricter — it caught a real generic-inference hole tsc missed. tsc
  stays the authoritative gate; tsgo adds fast coverage. New `typecheck:go`
  script for local use.

Fixes surfaced by the above:
- lsfg-vk InstallCard: `Select` inferred its generic as `string`, so the
  layer-version value flowed into a `LayerVersion` handler unchecked. Pin the
  generic explicitly (`Select<LayerVersion>`) — genuinely type-safe, no cast.
- recomp/lib/state.ts: 6 vestigial `state` params (the helpers now read fresh
  state via mutateState) flagged by noUnusedParameters — `_`-prefixed.

Audit note: the production `any`/suspect-cast surface is small (~51 across
~24 files) and almost entirely legitimate runtime/FFI boundaries (Electrobun
CEF imports resolved at runtime, Steam-webhelper patching). The 8 production
@ts-ignore are all on runtime-resolved electrobun/* imports.

tsc: 0 · tsgo: 0 · eslint: 0 errors · 364 tests pass.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* chore(ts): make tsgo the primary typecheck; close coverage gaps from review

Per follow-up: promote the Go-native compiler from a CI-only extra to the
main typecheck everywhere, and address PR-review findings.

- `typecheck` script now runs tsgo (primary); `tsc` moves to `typecheck:tsc`
  and is kept as a CI/release safety-net step since tsgo is still a -dev
  preview (can have false negatives) — belt-and-braces until it GAs.
- ci.yml + release.yml: tsgo is the primary typecheck step, tsc the backstop.
  Documented that tsgo must be invoked via `bun run` (its bin shim is
  `#!/usr/bin/env node`; bun runs it with no node installed).

Review findings folded in:
- Coverage gap (Medium): the shipped recomp install recipes under
  plugins/*/games/**/*.ts were type-checked by nothing (loaded via a
  runtime-computed dynamic import, so not even transitively). Added
  plugins/*/games/** and plugins/*/scripts/** to `include` — both compilers
  pass with zero new errors.
- Symmetry (Low): added **/*.spec.ts to `exclude` (only *.spec.tsx was listed).

tsc: 0 · tsgo: 0 · frozen-lockfile clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* chore(ts): enable noUncheckedIndexedAccess + fix all 323 errors (#208)

Turn on `noUncheckedIndexedAccess` — indexed access (arr[i], obj[key],
regex groups, .split() parts, array destructuring) now yields `T | undefined`,
catching a large class of latent "possibly undefined" bugs.

Fixed all 323 resulting errors across ~90 files, properly (no suppressions):
- Real guards / early-returns / `continue` where a value can genuinely be
  missing (parser rows, device lookups, empty collections).
- `?? fallback` where a sensible default exists.
- Non-null `!` ONLY where provably in-bounds — after a `.length` check, inside
  `0..len-1` loops, on required regex capture groups, or on `findIndex`
  results — each annotated with a short justifying comment.

No `any` / `as any` / `as unknown as` / `@ts-ignore` introduced; the one
pre-existing React-fiber `as unknown as` in qam.ts was preserved as-is.
Runtime behavior unchanged (guards only fire on inputs that would previously
have thrown or produced undefined downstream); parser test suites still pass.

tsgo: 0 · tsc: 0 · eslint: 0 errors · 364 tests pass.

Co-authored-by: Claude Opus 4.8 <[email protected]>

* refactor(ts): replace bare non-null assertions with guards and safe fallbacks

Review feedback on the noUncheckedIndexedAccess pass: ~159 of the fixes were
bare `!` assertions — compile-time-only claims that insert no runtime check,
so a wrong (or later-invalidated) assumption still flows `undefined` silently
downstream. Rework them into runtime-safe patterns, preserving exact behavior
for all real inputs:

- Bitwise/boolean numeric reads → `?? 0` (behavior-identical: `undefined &
  mask` already coerces to 0; HID report bytes, evdev bitmaps, capability
  bits, gamepad axes).
- Required regex capture groups → `?? ""` (group is always defined on a
  match; the fallback is dead code that removes the assertion).
- "Must exist" elements (length-checked [0], findIndex/splice results,
  Promise.all index-aligned rows) → hoisted guarded locals that log and
  degrade with the SAME action the surrounding code already used
  (continue/return/throw-the-same-error), so an impossible-today miss
  becomes a diagnosable warning instead of a silent undefined.
- Loop-bounded indexing → restructured to for...of / .entries() / paired
  iteration where it reads cleanly (removes the assertion entirely).
- injector.ts: `this.cdp!` sites → a `requireCdp()` accessor; definite-
  assignment `let tab!` → explicit `| undefined` with a post-loop guard.

Bare `!` reads remaining in the diff vs main: 2 (DAY_NAMES[date.getDay()]!,
provably 0-6 into a 7-element const, commented). Definite-assignment
declarations (`let x!: T`) kept where they're the standard sync-Promise-
executor pattern.

tsgo: 0 · tsc: 0 · eslint: 0 errors · 364 tests pass.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(vdf): warn on brace underflow instead of silently partial-parsing

Review finding on the noUncheckedIndexedAccess pass: parseVdf used to throw
on brace-underflowed (corrupt/truncated) VDF, and callers like playtime used
that throw as their malformed-file signal (skip the user). The added guards
made it silently return a partial parse instead — indistinguishable from a
complete one.

Keep the lenient parse (more robust than crashing), but log a one-shot
warning when underflow is detected so a partial result is diagnosable.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Claude Opus 4.8 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant