Skip to content

feat: add route contracts for state and search#32

Merged
sv2dev merged 2 commits into
mainfrom
feature/route-contracts
May 7, 2026
Merged

feat: add route contracts for state and search#32
sv2dev merged 2 commits into
mainfrom
feature/route-contracts

Conversation

@sv2dev
Copy link
Copy Markdown
Owner

@sv2dev sv2dev commented May 7, 2026

Summary

  • add route() contract helper for typed state and search metadata
  • thread typed search through NavOpts/router resolution types
  • enforce route metadata in typed router.go() calls
  • document the new API and add type coverage

Validation

  • npm run typecheck
  • npm test -- --run

Summary by CodeRabbit

  • New Features

    • Added search parameter typing support to route handlers with compile-time TypeScript validation for required metadata.
    • Router now enforces correct state and search shapes when navigating to typed routes.
  • Documentation

    • Enhanced README with comprehensive examples showing how to declare and use typed route state and search parameters.
  • Refactor

    • Strengthened underlying type system for improved TypeScript support across routing APIs and navigation metadata.

sv2dev added 2 commits April 3, 2026 00:36
Relax Routes index signature to allow heterogeneous state types per
handler while keeping the S generic for route guards. Add wildcard
"*" fallback in _NavigateRoutes so paths like /x/z correctly resolve
to the wildcard handler's state type.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Review Change Stack

Walkthrough

This PR extends the esroute type system to support typed search parameters alongside existing typed state. It introduces a RouteContract with a route() factory for declaring route handler metadata, threads a Search generic parameter through core types (NavMeta, NavOpts, Resolved, Router), and updates path traversal to extract and enforce metadata requirements via compile-time type checks.

Changes

Search Parameter and Contract System

Layer / File(s) Summary
Contract and Route Type System
packages/esroute/src/routes.ts
Introduces RouteContract, route() factory, conditional types (ContractState, ContractSearch, HasContract, HasContractState, HasContractSearch) for extracting metadata. Updates Resolve and Routes to carry Search extends Record<string,string> generic. Adds SearchOf<F>, NeedsSearch<F>, NavMetaFor<F>, NeedsNavMeta<F> type utilities.
Path Traversal and Handler Resolution
packages/esroute/src/routes.ts
Reworks RoutePaths recursion logic and introduces internal path walker (_Split, _PathParts, _NavigateRoutes). Updates HandlerFor<R, P> to resolve handlers from nested routes including wildcard (*) matching while preserving Search typing throughout traversal.
Navigation Options Base Types
packages/esroute/src/nav-opts.ts
NavMeta and StrictNavMeta now generic over S and Search extends Record<string,string>. NavOpts<S,Search> implements both with updated constructor overloads accepting StrictNavMeta<S,Search> or PathOrHref + NavMeta<S,Search>. Search parsing and default initialization aligned with Search type. go() callback accepts NavMeta<S,Search> and instantiates NavOpts<S,Search>.
Route Resolution with Search
packages/esroute/src/route-resolver.ts
Resolved<T,S,Search> and RouteResolver<T,S,Search> interfaces updated with Search generic and opts: NavOpts<S,Search>. Exported resolve() function threads generics through redirect loop and returns Promise<Resolved<T,S,Search>>. Internal getResolves and checkGuard use explicit generic signatures with Routes<any,any,any> and NavOpts<any,any> to preserve typing.
Router Interface and Factory
packages/esroute/src/router.ts
Router<T,S,R,Search>, RouterConf<T,S,R,Search>, and OnResolveListener<T,S,Search> gain Search generic parameter. Config fields (routes, notFound, onResolve) updated to Routes<T,S,Search>, Resolve<T,S,Search>, OnResolveListener<T,S,Search>. createRouter factory returns Router<T,S,R,Search> and internal _current and resolution preserve Search typing.
Router Navigation with Meta Requirements
packages/esroute/src/router.ts
Router.go() method overloads enforce typed NavMeta<S,Search> requirement when route declares metadata via NeedsNavMeta/NavMetaFor. Implementation constructs NavOpts<S,Search> and threads search through resolution. resolveCurrent upgrades options while preserving replace, search, state, hash fields. applyResolution and updateState helpers updated to Resolved<T,S,Search> and NavOpts<S,Search> types.
Type Safety and Runtime Tests
packages/esroute/src/router.spec.ts
Test fixture adds routes with explicit state/search contracts via route<state,search>(). New @ts-expect-error compile-time checks verify go() enforces required metadata, rejects missing state, missing search, and incorrect search types. Additional test verifies untyped routes reject providing state/search.
Documentation and Examples
packages/esroute/README.md
Updates "Typed state" section to "Typed state and search", demonstrating route<state,search>() handlers with TypeScript enforcement that router.go() fails without required metadata. Adds equivalent explicit NavOpts<state,search> handler signature. Documents state runtime defaulting to null when not provided.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • sv2dev/esroute#23: This PR builds directly on the same type-safe routing changes—extending NavOpts, RouteResolver, Router, and Routes with additional generics and contract-aware typing for search parameters.
  • sv2dev/esroute#27: Related changes to Router.go() overloads and type-level tests that forbid passing metadata for routes that don't declare it.
  • sv2dev/esroute#28: Related modifications to route typing, Router.go() overloads, and handler metadata extraction via NeedsState and route contracts.

Poem

🐰 With search and state now typed with care,
The router knows what metadata's there,
No more surprises at compile time,
Each route declares its shape so fine,
Contracts and generics dance in place,
TypeScript validates the nav with grace!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add route contracts for state and search' directly and clearly describes the main change: introducing route contracts that support typed state and search metadata.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/route-contracts

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/esroute/src/router.ts (1)

13-23: 💤 Low value

Consider aligning the Search default with the rest of the surface.

OnResolveListener, Router, RouterConf, and createRouter default Search to Record<string, string>, while NavMeta/NavOpts (in nav-opts.ts), Routes/Resolve (in routes.ts), and Resolved/RouteResolver (in route-resolver.ts) default it to Record<never, never>. With satisfies Routes<…> the explicit type usually wins, but bare aliases like Router<string> vs Resolved<string> will silently differ in search key openness, which can surprise consumers reading router.current.search vs a directly constructed NavOpts.

If the open default in router.ts is intentional (because router-level types are aggregated across all routes), it's worth a brief comment near these declarations; otherwise consider standardizing on Record<never, never>.

♻️ Possible alignment
 export type OnResolveListener<
   T,
   S = never,
-  Search extends Record<string, string> = Record<string, string>
+  Search extends Record<string, string> = Record<never, never>
 > = (resolved: Resolved<T, S, Search>) => void;
 export interface Router<
   T = any,
   S = never,
   R extends RawRoutes = RawRoutes,
-  Search extends Record<string, string> = Record<string, string>
+  Search extends Record<string, string> = Record<never, never>
 > {

…and analogously in RouterConf and createRouter.

Also applies to: 87-92, 115-125

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/esroute/src/router.ts` around lines 13 - 23, The Search generic
default in OnResolveListener, Router (and related RouterConf/createRouter) is
currently Record<string, string> while other surface types (NavMeta/NavOpts,
Routes/Resolve, Resolved/RouteResolver) default to Record<never, never>, causing
inconsistent openness of the search key; either change the Search default in
OnResolveListener, Router, RouterConf and createRouter to Record<never, never>
to match the rest of the API, or add a short comment above these declarations
documenting that the router-level types are intentionally open because they
aggregate across routes—update the generic defaults (or add the comment) in the
symbols OnResolveListener, Router, RouterConf, and createRouter accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/esroute/src/router.ts`:
- Around line 13-23: The Search generic default in OnResolveListener, Router
(and related RouterConf/createRouter) is currently Record<string, string> while
other surface types (NavMeta/NavOpts, Routes/Resolve, Resolved/RouteResolver)
default to Record<never, never>, causing inconsistent openness of the search
key; either change the Search default in OnResolveListener, Router, RouterConf
and createRouter to Record<never, never> to match the rest of the API, or add a
short comment above these declarations documenting that the router-level types
are intentionally open because they aggregate across routes—update the generic
defaults (or add the comment) in the symbols OnResolveListener, Router,
RouterConf, and createRouter accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d97f5480-cb3e-4a22-93a4-886c76b0235c

📥 Commits

Reviewing files that changed from the base of the PR and between 42b56e2 and e8098a2.

📒 Files selected for processing (6)
  • packages/esroute/README.md
  • packages/esroute/src/nav-opts.ts
  • packages/esroute/src/route-resolver.ts
  • packages/esroute/src/router.spec.ts
  • packages/esroute/src/router.ts
  • packages/esroute/src/routes.ts

@sv2dev sv2dev merged commit 35ab4a6 into main May 7, 2026
2 checks passed
@sv2dev sv2dev deleted the feature/route-contracts branch May 7, 2026 21:50
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