fix(hydrate): make R inferable from options.layers - #81
Merged
Conversation
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>
Deploying effex with
|
| Latest commit: |
bb51984
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://7865879d.effex.pages.dev |
| Branch Preview URL: | https://fix-hydrate-generic-layers.effex.pages.dev |
Deploying effex-api with
|
| 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 |
…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>
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.
Summary
Type-signature follow-up to #78. Two problems fixed:
1. The original type error the user reported.
hydrate's element parameter was locked toElement<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 thoughoptions.layerswas 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 (
Rgeneric + inferred bidirectionally) let TS widenRfrom the element's requirements. Sohydrate(NeedsFoo(), root)with nooptions.layersstill compiled — TS inferredR = Foo, options was undefined, no error surface. That's the exact footgun the cast was creating.Fix:
HydrateOptionsandhydrateare now generic over the layer typeL. Inference flows one way —Lis inferred solely fromoptions.layers(viaNoInferon the element'sRslot), then the element must fitFramework | 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)
options.layersFooFoo{}FooBarLayerFooFooLayerFoo | BarFooLayerFoo | BarLayer.merge(FooLayer, BarLayer)Users get type-level assurance that every service the element uses is provided.
Also
Removed the now-obsolete casts from:
packages/create-effexSSG + SSR templatesapps/docs/src/client.tspackages/dom's own hydrate regression test (was usingas neverfor the same reason)Test plan
packages/dom+packages/routersuite: 465 passedapps/docswith a scratch typecheck fileapps/docstypechecks clean with no casts🤖 Generated with Claude Code