feat: pass route definition to route components via route prop - #244
Conversation
Route components now receive their own route definition object as the route prop, typed so it can be passed directly to the typed hooks (useRouteParams, useRouteState, useRouteData). This enables type-safe hook usage when the route definition cannot be imported directly, such as when route definitions are managed internally by a framework implementing file-system based routing. Co-Authored-By: Claude <noreply@anthropic.com>
|
Request: tighten the declared fields of
Why now, in the context of this PR:
What tightening would and wouldn't affect:
Since the type shipped in 1.3.0, removing declared members is strictly a breaking type change and should at minimum get a changelog note (whether it warrants a major is your call given the type is one minor old). A zero-risk middle ground would be keeping Generated by Claude Code |
Drop the children, exact, and requireChildren declared fields from TypefulOpaqueRouteDefinition, keeping only path among the definition fields. The type is meant to be an opaque, brand-carrying handle whose Id/Params/State/Data info is consumed via the Extract* helpers and typed hooks. With the new route prop exposing these values to components that did not author them, this is the cheapest moment to tighten the surface before direct field reads appear in the wild. PartialRouteDefinition already declares only path, so it is consistent as-is. Runtime behavior is unchanged; the change is purely type-level. Note: this is a breaking type-level change for code that read def.children / def.exact / def.requireChildren on values typed as TypefulOpaqueRouteDefinition, or accessed those properties through the RouteDefinition union. The in-repo entries.tsx path collectors now use a local structural view instead. Co-Authored-By: Claude <noreply@anthropic.com>
|
Done in cf1e9ff — implemented the middle ground you outlined: Details:
All packages typecheck, 347 tests pass, lint/format clean. Generated by Claude Code |
|
Ah, um... the Please revert the breaking change. Instead, give the tightened type a new name and use it for the new route prop. |
…e route prop Restore the children, exact, and requireChildren declared fields on TypefulOpaqueRouteDefinition — reading them (e.g. walking children to collect static paths in entries.tsx) is a supported use case that the tightening would have broken. Instead, introduce a new exported RouteHandle<Id, Params, State, Data> type: an opaque handle carrying only the phantom type info and path. TypefulOpaqueRouteDefinition now extends RouteHandle, and the route prop on RouteComponentProps / RouteComponentPropsWithData is typed as RouteHandle, so components see the tightened surface while authored definitions keep their full declared shape. The typed hooks accept RouteHandle values structurally, unchanged. Co-Authored-By: Claude <noreply@anthropic.com>
|
Done in c953758 — the tightening is reverted and the tightened shape now lives under a new name used only for the prop.
Type tests updated: the opacity assertions now target Generated by Claude Code |
The typed hooks (useRouteParams, useRouteState, useRouteData) already accepted RouteHandle values, but only structurally: the constraint arm was TypefulOpaqueRouteDefinition, which a handle satisfies solely because Typeful's extra declared fields are all optional. Rewrite the hook constraints and the Extract* helpers' second branch in terms of RouteHandle so acceptance is guaranteed by the extends relationship rather than by structural coincidence. Every existing call site is unaffected since TypefulOpaqueRouteDefinition extends RouteHandle. Also add type tests covering RouteHandle-typed values against the hooks and Extract* helpers, and mention RouteHandle in the type-safe hooks docs. Co-Authored-By: Claude <noreply@anthropic.com>
Summary
Route components now receive an opaque handle to their own route definition as the
routeprop, typed so it can be passed directly to the typed hooks (useRouteParams,useRouteState,useRouteData).This enables type-safe hook usage when the route definition cannot be imported directly — for example, when route definitions are managed internally by a framework implementing file-system based routing. In such setups, the
routeprop is the only way for a user-authored component to reach its definition. In ordinary SPA apps where definitions are importable, existing patterns continue to work unchanged.Changes
RouteHandle<Id, Params, State, Data>type: an opaque handle carrying only the phantom type info andpath?.TypefulOpaqueRouteDefinitionnow extendsRouteHandle, so every authored definition is assignable to the handle; its own declared fields (children,exact,requireChildren) are unchanged. Exported from the main entry.RouteComponentPropsgains a requiredrouteprop typed asRouteHandle<string, TParams, TState, unknown>;RouteComponentPropsWithDatanarrows it to carryTData. Theroute()/routeState()overloads and the typed hooks are untouched — hooks accept aRouteHandlestructurally.RouteRendererinjectsroute={match.route}for function components (both loader and non-loader branches). The prop is referentially identical to the registered definition — in the two-phasebindRouteflow, that is the bound (full) definition. JSX-element components are unaffected, as before.routeProp.test.tsxcovers prop identity for plain, loader, id-less (nearest-context fallback),bindRoute, and nested routes; type-level tests verify the prop's type, that all three typed hooks infer correctly fromprops.route(including a component authored againstRouteComponentPropsOf), thatRouteHandledeclares onlypathamong the definition fields, and thatTypefulOpaqueRouteDefinitionkeeps its declared fields and extendsRouteHandle.routeadded to the props listings on the Types API page, aRouteHandleentry on the Types API page and reference index, plus a "The route prop" subsection with an example in the Type Safety guide.Notes
unknown(notundefined) because interface extension requires the narrowedTDatato be assignable to the base's slot.RouteComponentProps, code that renders a route component manually with hand-built props (tests, storybook) must now supply aroute. Component authors are unaffected — components declaring fewer props remain assignable.🤖 Generated with Claude Code
https://claude.ai/code/session_01U9kFBcxBJ9ForaLHfSug9E