Skip to content

feat(pages): type useParams() from the page's URL pattern - #685

Merged
cpojer merged 3 commits into
voidzero-dev:mainfrom
magurotuna:feat/typed-use-params
Nov 5, 2026
Merged

cpojer merged 3 commits into
voidzero-dev:mainfrom
magurotuna:feat/typed-use-params

Conversation

@magurotuna

@magurotuna magurotuna commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Pass a page's URL pattern as the type argument of useParams() to type its params.

const { id } = useParams<'/users/:id'>(); // id: string
useParams<'/users/:id'>().idd;            // Type error
useParams<'/userz/:id'>();                // Type error: no such page
  • Type-only: params are returned as before, with no runtime check.
  • Works in all four adapters. useParams() and useParams<{ id: string }>() are unchanged, and no config flag is needed.
  • Only page patterns are accepted, not HTTP routes.
  • void/routes exports PagePathMap, PagePath and PageParams<P>. Both the generated .void/routes.d.ts and the package declare them.

@yangshun

yangshun commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Note that this PR adds runtime checks to useParams, so it can now bring down a page (might not be a bad thing, but it's new behavior) if the param shapes don't match.

I'm not sure if we want to do runtime checks within useParams. No other frameworks except TanStack Start do runtime checks within useParams.

If we're ok with just typechecking, consider this instead:

useParams<'/posts/:id'>();

or use your newly-added PageParams util:

useParams<PageParams<'/posts/:id'>>();

Since PageParams<'/posts/:id'> returns an object, useParams doesn't even require any changes.

@cpojer

cpojer commented Nov 3, 2026

Copy link
Copy Markdown
Member

I also prefer doing typechecking only and not failing during runtime for this.

@magurotuna

Copy link
Copy Markdown
Member Author

That makes sense, thanks for the insight! I'll make this static-check only

`useParams<'/users/:id'>()` now returns `{ id: string }` in all four
adapters. The pattern must be one of the app's pages, so a misspelled
param or page is a type error. This is a type-only change: the params
are returned as before, with no runtime check.

- The generated `.void/routes.d.ts` adds `PagePathMap` (page patterns
  and their param names, without HTTP or WebSocket routes), plus
  `PagePath` and `PageParams<P>`. `void/routes` declares the same types
  for apps without generated declarations.
- An unnamed catch-all (`[...]`) has no param key, matching
  `c.req.param()`. A union of patterns gives a union of params.
- `useParams()` and `useParams<{ id: string }>()` keep their types.
@magurotuna
magurotuna force-pushed the feat/typed-use-params branch from a27aaf1 to a616364 Compare November 4, 2026 16:12
@magurotuna
magurotuna marked this pull request as ready for review November 4, 2026 16:46
The conditional return type broke generic wrappers such as
`function f<T extends Record<string, string>>(): T { return useParams<T>(); }`,
and `ReturnType<typeof useParams>` included `{}` for static pages. Use
overloads instead: the record generic first, then the page pattern, then
a plain `Record<string, string>` signature for `ReturnType`.
@magurotuna

Copy link
Copy Markdown
Member Author

Ok, switched to type-check only, and I confirmed that the existing behavior (no type param, or non-path type param such as useParams<{ id: string }> is kept. Ready for another look

@magurotuna
magurotuna requested review from cpojer and yangshun November 5, 2026 02:59

@cpojer cpojer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Awesome!

@cpojer
cpojer merged commit 8fe70d1 into voidzero-dev:main Nov 5, 2026
18 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.

3 participants