Skip to content

fix(webgpu): narrow useThree/useFrame to WebGPURootState on the /webgpu entry - #3860

Merged
DennisSmolek merged 1 commit into
v10from
fix/3851-webgpu-entry-types
Aug 11, 2026
Merged

fix(webgpu): narrow useThree/useFrame to WebGPURootState on the /webgpu entry#3860
DennisSmolek merged 1 commit into
v10from
fix/3851-webgpu-entry-types

Conversation

@DennisSmolek

Copy link
Copy Markdown
Member

Fixes #3851.

The entry already re-exported WebGPURootState as RootState, but useThree and useFrame arrived via export * from '../core' still typed against the base RootState, whose renderer is the R3FRenderer union (WebGLRenderer included). So anything WebGPU-only needed a cast, on an entry point that has already committed to WebGPU:

const renderer = useThree((s) => s.renderer)
renderer.compute(node)
// ~~~~~~~ Property 'compute' does not exist on type 'R3FRenderer'

Approach

Both hooks are re-declared against WebGPURootState. Explicit exports shadow the star re-export in both ESM and TypeScript, so this is types-only — the exported values are the core implementations, untouched. No runtime cost, and no second code path to keep in sync. There's a test pinning useThree === useThreeCore so those can't quietly drift apart.

The two signatures are structurally incompatible (the selector parameter makes them contravariant), so the re-type has to go through unknown. It's sound: WebGPURootState is the same object the base hook already returns, with renderer / gl / internal narrowed to what this entry guarantees at runtime.

Testing

The assertions that matter are compile-time. The repo's tsconfig includes packages/**/*, so pnpm typecheck compiles the new assertion file on every CI run — a types regression fails the build. The typeAssertions() function is never called; its body only has to compile.

Confirmed it fails pre-fix with exactly the errors from the issue:

entry-types.test.tsx:28 - error TS2322: Type 'R3FRenderer' is not assignable to type 'WebGPURenderer'.
  const renderer: WebGPURenderer = useThree((s) => s.renderer)
entry-types.test.tsx:39 - error TS2740: Type 'WebGLRenderer' is missing ... 'WebGPURenderer'
  const gl: WebGPURenderer = useThree((s) => s.gl)
entry-types.test.tsx:45 - error TS2322: Type 'R3FRenderer' is not assignable to type 'WebGPURenderer'.
  const frameRenderer: WebGPURenderer = frameState.renderer

Coverage includes the DepthAttachmentSync pattern the issue calls out, which previously needed (state.renderer as unknown as { backend?: ... }).backend.

Verified through the build

Since the issue is about the shipped dist/webgpu/index.d.ts, I checked the narrowing survives declaration emit rather than only holding in source:

declare const useThree: UseThreeWebGPU;
declare const useFrame: UseFrameWebGPU;

and both appear in the value export list exactly once — the shadowing doesn't produce a duplicate or ambiguous export.

  • pnpm typecheck ✅ (and fails pre-fix, as above)
  • pnpm build + pnpm verify-bundles + pnpm verify-types
  • pnpm test ✅ 582 passed, 46 files
  • pnpm eslint / pnpm format

Scope note

Narrowed useThree and useFrame, the two the issue names. Other core hooks still take base RootState; happy to extend if you'd rather do the whole surface at once, but I kept this minimal for alpha 4.

🤖 Generated with Claude Code

…pu entry

The entry already re-exported WebGPURootState as RootState, but useThree and
useFrame arrived via `export * from '../core'` still typed against the base
RootState, whose `renderer` is the R3FRenderer union (WebGLRenderer included).
So anything WebGPU-only needed a cast on an entry point that has already
committed to WebGPU:

    const renderer = useThree((s) => s.renderer)
    renderer.compute(node)
    // Property 'compute' does not exist on type 'R3FRenderer'

which is exactly the friction the split entry points exist to remove.

Both hooks are now re-declared against WebGPURootState. Explicit exports shadow
the star re-export in both ESM and TS, so this is types-only: the exported
values are the core implementations untouched, no runtime cost and no second
code path to keep in sync. A test pins that identity so the two cannot drift.

The signatures are structurally incompatible (the selector parameter makes them
contravariant), so the re-type goes through `unknown`. It is sound --
WebGPURootState is the same object the base hook already returns, with
renderer/gl/internal narrowed to what this entry guarantees at runtime.

Guarded by compile-time assertions rather than runtime ones: tsconfig includes
packages/**, so `pnpm typecheck` compiles the assertion file on every run.
Confirmed it fails pre-fix with the three errors from the issue, and that the
narrowing survives declaration emit -- dist/webgpu/index.d.ts now carries
`declare const useThree: UseThreeWebGPU`, exported exactly once.

Fixes #3851

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@DennisSmolek
DennisSmolek merged commit da7a768 into v10 Aug 11, 2026
3 checks passed
@DennisSmolek
DennisSmolek deleted the fix/3851-webgpu-entry-types branch August 11, 2026 00:03
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