chore(types): remove as unknown as casts where they can be (#1466)#1484
Merged
Conversation
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>
Contributor
There was a problem hiding this comment.
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.defergeneric over the deferred function’s argument tuple, enabling strongly-typed defer call sites. - Replace several
as unknown ascasts with safer typing strategies: structural narrowing (camera statics), concrete builtinBodynarrowing in builtin raycast, and a WebGL2 capability check usinginstanceof WebGLRenderer. - Fix an incorrect JSDoc union for builtin
Body.shapesand remove a destroy-timesettingscast inParticleEmitter.
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.
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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, orinstanceofnarrowing.Removed
utils/function.tsdefer(func: (...args: unknown[]) => unknown, ...)defer<TArgs>(func: (...args: TArgs) => unknown, ...)state/state.ts(× 2)defer(_switchState as unknown as ...)defer(_switchState, ...)particles/emitter.tsthis.settings = undefined as unknown as ParticleEmitterSettingssettingsafterdestroy(), image-clear stays)physics/builtin/raycast.tsobjB.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 typingphysics/broadphase/quadtree.ts(× 2)this.insertContainer(child as unknown as Container){ getChildren(): ContainerOrChild[] }shape; cast droppedphysics/broadphase/octree.ts(× 2)state/stage.ts(× 2)StageCameraClass as unknown as SortAwareCameraClassas SortAwareCameraClass(structural shape narrowing)application.ts(this.renderer as unknown as { WebGLVersion?: number }).WebGLVersion !== 2!(this.renderer instanceof WebGLRenderer) || this.renderer.WebGLVersion !== 2Intentionally 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.constructoris typed asFunctionwith no statics; the structural shape conversion needs theunknownstep.Test plan
pnpm test:typesclean120 / 120 files, 3969 passed, 0 failed, 13 skipped (same as master baseline)pnpm buildruns lint + types as part of CI)🤖 Generated with Claude Code