Skip to content

fix(hydrate): make R inferable from options.layers - #81

Merged
jonlaing merged 2 commits into
mainfrom
fix/hydrate-generic-layers
Aug 4, 2026
Merged

fix(hydrate): make R inferable from options.layers#81
jonlaing merged 2 commits into
mainfrom
fix/hydrate-generic-layers

Conversation

@jonlaing

@jonlaing jonlaing commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Summary

Type-signature follow-up to #78. Two problems fixed:

1. The original type error the user reported. hydrate's element parameter was locked to Element<A, never, RendererContext | ControlCtx | SuspenseBoundaryCtx>, so any element that also required a user-provided service (NavigationContext, RouteDataProvider, …) had to be cast — as never / as unknown as Element<HTMLElement> — even though options.layers was providing exactly those services at runtime. Build still worked (types don't affect runtime), but the error was noisy.

2. Casts hide missing layers. The first attempt at this PR (R generic + inferred bidirectionally) let TS widen R from the element's requirements. So hydrate(NeedsFoo(), root) with no options.layers still compiled — TS inferred R = Foo, options was undefined, no error surface. That's the exact footgun the cast was creating.

Fix: HydrateOptions and hydrate are now generic over the layer type L. Inference flows one way — L is inferred solely from options.layers (via NoInfer on the element's R slot), then the element must fit Framework | Layer.Layer.Success<L>. TS extracts the layer's provided services and requires the element's requirements to be covered by them.

Behavior matrix (all verified)

Element requires options.layers Result
Nothing extra omitted ✅ OK
Foo omitted ❌ Error
Foo {} ❌ Error
Foo BarLayer ❌ Error
Foo FooLayer ✅ OK
Foo | Bar FooLayer ❌ Error
Foo | Bar Layer.merge(FooLayer, BarLayer) ✅ OK

Users get type-level assurance that every service the element uses is provided.

Also

Removed the now-obsolete casts from:

  • packages/create-effex SSG + SSR templates
  • apps/docs/src/client.ts
  • packages/dom's own hydrate regression test (was using as never for the same reason)

Test plan

  • Full packages/dom + packages/router suite: 465 passed
  • Behavior matrix above verified in apps/docs with a scratch typecheck file
  • apps/docs typechecks clean with no casts

🤖 Generated with Claude Code

hydrate's element parameter was locked to
`Element<A, never, RendererContext | ControlCtx | SuspenseBoundaryCtx>`,
which meant any element that also required a user-provided service
(NavigationContext, RouteDataProvider, etc.) had to be cast — `as never`
or `as unknown as Element<HTMLElement>` — even though `options.layers`
was providing exactly those services at runtime.

HydrateOptions and hydrate are now generic over `R`. Whatever services
`options.layers` provides show up as the element's `R`, so the intended
pattern typechecks with no casts:

    hydrate(App(), root, { layers: Platform.makeClientLayer(router) });

Removed the now-obsolete casts from:
- packages/create-effex SSG + SSR templates
- apps/docs/src/client.ts
- packages/dom's own hydrate regression test

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 4, 2026

Copy link
Copy Markdown

Deploying effex with  Cloudflare Pages  Cloudflare Pages

Latest commit: bb51984
Status: ✅  Deploy successful!
Preview URL: https://7865879d.effex.pages.dev
Branch Preview URL: https://fix-hydrate-generic-layers.effex.pages.dev

View logs

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 4, 2026

Copy link
Copy Markdown

Deploying effex-api with  Cloudflare Pages  Cloudflare Pages

Latest commit: bb51984
Status: ✅  Deploy successful!
Preview URL: https://5de0b54e.effex-api.pages.dev
Branch Preview URL: https://fix-hydrate-generic-layers.effex-api.pages.dev

View logs

…layers

Follow-up: the earlier commit made `R` generic but relied on TS inferring
`R` from both element and options — so passing `hydrate(NeedsFoo(), root)`
without any layers still compiled (R widened to Foo, but options was
undefined). No compile-time protection against forgetting a service.

Refactored so `L` is inferred solely from `options.layers`, and the
element's `R` position is `NoInfer<Layer.Layer.Success<L>>`. Inference
flows one way — layers in, element requirements out. Forgetting to
provide a service the element uses is now a type error.

Matrix of behaviors, all verified:
- element needs Foo, no options → ERROR
- element needs Foo, options={} → ERROR
- element needs Foo, wrong layer provided → ERROR
- element needs Foo+Bar, only Foo layer → ERROR
- element needs Foo, correct layer → OK
- element needs Foo+Bar, merged Foo|Bar layer → OK
- element needs nothing extra, no options → OK (2-arg call still works)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jonlaing
jonlaing merged commit de534f6 into main Aug 4, 2026
4 checks passed
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