Skip to content

chore(types): remove as unknown as casts where they can be (#1466)#1484

Merged
obiot merged 2 commits into
masterfrom
chore/1466-remove-as-unknown-as-casts
Jun 1, 2026
Merged

chore(types): remove as unknown as casts where they can be (#1466)#1484
obiot merged 2 commits into
masterfrom
chore/1466-remove-as-unknown-as-casts

Conversation

@obiot
Copy link
Copy Markdown
Member

@obiot obiot commented Jun 1, 2026

Closes #1466.

Cleans up most of the as unknown as ... cast sites the engine carried. Each cast was a "we lied to TypeScript about the runtime type" patch — most could be replaced with proper generics, structural types, or instanceof narrowing.

Removed

File Before After
utils/function.ts defer(func: (...args: unknown[]) => unknown, ...) defer<TArgs>(func: (...args: TArgs) => unknown, ...)
state/state.ts (× 2) defer(_switchState as unknown as ...) defer(_switchState, ...)
particles/emitter.ts this.settings = undefined as unknown as ParticleEmitterSettings Line deleted (dead — nothing reads settings after destroy(), image-clear stays)
physics/builtin/raycast.ts objB.body as unknown as { shapes, getShape } objB.body as Body (concrete builtin Body)
physics/builtin/body.js @type {Polygon[]|Line[]|Ellipse[]|Point|Point[]} @type {(Polygon|Line|Ellipse|Point)[]} — fixed wrong JSDoc that broke downstream typing
physics/broadphase/quadtree.ts (× 2) this.insertContainer(child as unknown as Container) Signature changed to structural { getChildren(): ContainerOrChild[] } shape; cast dropped
physics/broadphase/octree.ts (× 2) same same
state/stage.ts (× 2) StageCameraClass as unknown as SortAwareCameraClass as SortAwareCameraClass (structural shape narrowing)
application.ts (this.renderer as unknown as { WebGLVersion?: number }).WebGLVersion !== 2 !(this.renderer instanceof WebGLRenderer) || this.renderer.WebGLVersion !== 2

Intentionally kept (documented)

  • audio/backend.ts:188(Howler as unknown as { _muted: boolean })._muted. Reaches Howler's private field; no public getter exists.
  • state/stage.ts:233.constructor as unknown as SortAwareCameraClass. Object.constructor is typed as Function with no statics; the structural shape conversion needs the unknown step.

Test plan

  • pnpm test:types clean
  • Full Vitest suite: 120 / 120 files, 3969 passed, 0 failed, 13 skipped (same as master baseline)
  • Engine builds clean (pnpm build runs lint + types as part of CI)

🤖 Generated with Claude Code

Cleans up most of the `as unknown as ...` cast sites the engine
carried. Each cast was a "we lied to TypeScript about the runtime
type" patch — most could be replaced with proper generics, structural
types, or `instanceof` narrowing.

**Removed:**

- `utils/function.ts` — `defer` is now generic over the target
  function's tail-args tuple (`defer<TArgs extends unknown[]>`). Callers
  pass the function's actual parameters; the compiler infers `TArgs`.
- `state/state.ts` — two `_switchState as unknown as
  (...args: unknown[]) => unknown` casts dropped. Generic `defer`
  accepts the typed function directly.
- `particles/emitter.ts` — `this.settings = undefined as unknown as
  ParticleEmitterSettings` deleted entirely. The line was dead: no read
  site checks `this.settings` after destroy, and once the emitter
  itself is unreferenced, `settings` follows. The image clear (the only
  load-bearing line in destroy) stays.
- `physics/builtin/raycast.ts` — `objB.body as unknown as {
  shapes, getShape }` → `as Body`. The raycast walks the BuiltinAdapter
  broadphase, so `objB.body` is always a concrete builtin `Body`. While
  here, also fixed the JSDoc on `Body.shapes` which incorrectly
  included a non-array `Point` in the union (`Polygon[]|Line[]|
  Ellipse[]|Point|Point[]` → `(Polygon|Line|Ellipse|Point)[]`).
- `physics/broadphase/quadtree.ts` + `octree.ts` — four casts dropped.
  `insertContainer` / `removeContainer` now accept a structural
  `{ getChildren(): ContainerOrChild[] }` shape rather than the
  concrete `Container` class, so the recursive walk doesn't need an
  `as unknown as Container` to bridge the duck-typed children. Unused
  `Container` imports removed alongside.
- `state/stage.ts` — two of three `as unknown as SortAwareCameraClass`
  casts dropped (constructor-shape narrowing works as a direct `as`).
  The third (`.constructor as unknown as ...`) stays — `Object.constructor`
  is typed as `Function` with no statics, and the structural shape
  conversion needs the `unknown` step.
- `application.ts` — `(this.renderer as unknown as { WebGLVersion?: number
  }).WebGLVersion !== 2` replaced with `instanceof WebGLRenderer`
  narrowing. The class is already imported (since #1479's WebGL-required
  fix), so no extra runtime cost, and the narrowing makes
  `WebGLVersion` typed correctly.

**Intentionally kept (documented):**

- `audio/backend.ts:188` — `(Howler as unknown as { _muted: boolean
  })._muted`. Reaches Howler's private field; no public getter exists.
- `state/stage.ts:233` — `.constructor as unknown as SortAwareCameraClass`
  (see above).

**Test impact:** none. Full suite stays at 3969 passed / 13 skipped /
0 failed (identical to the pre-change baseline). Type-check clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 1, 2026 08:33
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR removes most remaining as unknown as ... type assertions across the engine by improving typings (generics / structural types) and using proper runtime narrowing where applicable, reducing “lie-to-TypeScript” casts while keeping runtime behavior intact.

Changes:

  • Make utils/function.defer generic over the deferred function’s argument tuple, enabling strongly-typed defer call sites.
  • Replace several as unknown as casts with safer typing strategies: structural narrowing (camera statics), concrete builtin Body narrowing in builtin raycast, and a WebGL2 capability check using instanceof WebGLRenderer.
  • Fix an incorrect JSDoc union for builtin Body.shapes and remove a destroy-time settings cast in ParticleEmitter.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/melonjs/src/utils/function.ts Make defer generic to preserve call-site argument types and remove forced casts.
packages/melonjs/src/state/state.ts Drop as unknown as casts at defer(_switchState, ...) call sites.
packages/melonjs/src/state/stage.ts Replace constructor-to-statics cast with a simpler structural assertion.
packages/melonjs/src/physics/builtin/raycast.ts Narrow objB.body to builtin Body instead of duck-typing via unknown.
packages/melonjs/src/physics/builtin/body.js Correct shapes JSDoc type to always be an array for downstream TS consumers.
packages/melonjs/src/physics/broadphase/quadtree.ts Replace Container casts by typing against a structural getChildren() shape.
packages/melonjs/src/physics/broadphase/octree.ts Same as QuadTree: structural container typing to remove unknown casts.
packages/melonjs/src/particles/emitter.ts Remove destroy-time settings nullification cast; keep image reference clearing.
packages/melonjs/src/application/application.ts Use instanceof WebGLRenderer to type-narrow the WebGL2 warning check.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/melonjs/src/physics/broadphase/quadtree.ts Outdated
Comment thread packages/melonjs/src/physics/broadphase/octree.ts Outdated
…rsion (#1466)

Addresses Copilot review on #1484:

1. Replace the `as Required<Pick<ContainerOrChild, "getChildren">>`
   assertion in the recursive `insertContainer(child)` call with a
   real type predicate `hasGetChildren(c)`. TypeScript doesn't narrow
   optional methods via `typeof obj.method === "function"` directly,
   but the type predicate carries the narrowing through to the
   recursive call site cleanly.

2. Fix CI lint failure: the inline structural param types
   `{ getChildren(): ContainerOrChild[] }` tripped
   `jsdoc/require-param`, which wanted a nested
   `@param container.getChildren` entry at every call site. Named the
   shapes as `ContainerLike` / `ContainerLikeOptional` aliases so the
   JSDoc lint stops at the named-type boundary.

Both quadtree.ts and octree.ts get the same fix. No behaviour change;
3969 / 13 skipped / 0 failed, build + lint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@obiot obiot merged commit 70295ca into master Jun 1, 2026
6 checks passed
@obiot obiot deleted the chore/1466-remove-as-unknown-as-casts branch June 1, 2026 09:38
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.

Type-cleanup: remove remaining as unknown as casts

2 participants