useTypedParams
Read URL path parameters typed to the route's :segments — no manual casting.
#What it does
useTypedParams reads URL path parameters (the :segments in your route path) and types them directly from the route's path string. TypeScript extracts the param names automatically — no manual type annotations, no as string casts scattered across components.
This is the path-param counterpart to useTypedSearchParams:
| Hook | Reads from | Example |
|---|---|---|
useTypedParams | URL path — /users/:userId | { userId: string } |
useTypedSearchParams | Query string — ?page=1&sort=asc | { page: string, sort: string } |
#The problem it solves
// ❌ Without it — useParams returns Record<string, string | undefined>
// TypeScript has no idea what params exist
const params = useParams();
const conversationId = params.conversationId; // string | undefined — no path context
const id = stringToId<Conversation.Id>(conversationId!); // have to assert !// ✅ With useTypedParams — param names inferred from the path literal
const { conversationId } = useTypedParams(AppRoutes.inbox.conversation);
// TypeScript inferred { conversationId: string } from '/inbox/conversations/:conversationId'
// No ! needed — the params are guaranteed by the router matching the route
const id = stringToId<Conversation.Id>(conversationId);#Signature
import { useTypedParams } from '@void-snippets/react';
function useTypedParams<P extends string, S>(
route: ProcessedRoute<P, S>
): StringifiedRouteParams<P>Where StringifiedRouteParams maps every path parameter to string:
// Path: '/inbox/conversations/:conversationId'
// StringifiedRouteParams<...> resolves to:
{ conversationId: string }
// Path: '/orgs/:orgId/projects/:projectId'
// StringifiedRouteParams<...> resolves to:
{ orgId: string; projectId: string }
// Path: '/files/:path?' (optional segment)
// StringifiedRouteParams<...> resolves to:
{ path?: string }#Parameters
| Parameter | Type | Description |
|---|---|---|
route | ProcessedRoute | A leaf from createRouteContract. Used only for TypeScript inference — the value is never read at runtime. |
The
_routeparameter is erased at runtime. You are not passing configuration — you are giving TypeScript a reference to infer the path stringPand extract its parameter names.
#Return value
An object with one string property per named path segment. All values are string at runtime — React Router's useParams() always returns strings, regardless of how build() typed them as string | number.
#Working with branded IDs
Because all params are plain strings, the idiomatic pattern is to cast to a branded ID immediately after reading:
import { useTypedParams } from '@void-snippets/react';
import { stringToId } from '@void-snippets/core';
import type { Conversation } from './conversation.types';
import { AppRoutes } from '@/routes';
function ConversationPage() {
const { conversationId } = useTypedParams(AppRoutes.inbox.conversation);
// conversationId is string — safe to pass to stringToId
const id = stringToId<Conversation.Id>(conversationId);
const { item: conversation, isLoading } = conversationHooks.useGet(id);
// ...
}#Full example — multiple params
// routes.ts
export const AppRoutes = createRouteContract({
orgs: {
project: defineRoute('/orgs/:orgId/projects/:projectId', {
breadcrumb: 'Project',
permissions: ['ORG_MEMBER'],
}).search<{ tab?: 'overview' | 'tasks' | 'settings' }>(),
},
});// ProjectPage.tsx
function ProjectPage() {
// Path params from the URL
const { orgId, projectId } = useTypedParams(AppRoutes.orgs.project);
// TypeScript knows: { orgId: string; projectId: string }
// No useParams(), no non-null assertions, no Record<string, string | undefined>
// Search params from the query string
const { search, setSearch } = useTypedSearchParams(AppRoutes.orgs.project);
// TypeScript knows: { tab?: 'overview' | 'tasks' | 'settings' }
const id = stringToId<Project.Id>(projectId);
const { item: project, isLoading } = projectHooks.useGet(id);
if (isLoading || !project) return <Skeleton />;
return (
<div>
<h1>{project.name}</h1>
<nav>
{(['overview', 'tasks', 'settings'] as const).map(tab => (
<button
key={tab}
onClick={() => setSearch({ tab })}
aria-current={search.tab === tab || (!search.tab && tab === 'overview')}
>
{tab}
</button>
))}
</nav>
{/* render the active tab */}
</div>
);
}#How it works internally
useTypedParams is a thin typed wrapper around React Router's useParams():
export function useTypedParams<P extends string, S>(
_route: ProcessedRoute<P, S>, // type-only — never read
): StringifiedRouteParams<P> {
const params = useParams(); // Readonly<Record<string, string | undefined>>
return params as unknown as StringifiedRouteParams<P>;
// Cast is safe: the router only activates this component when the path
// matches, so every declared segment is guaranteed to be present.
}The safety of the cast relies on the router configuration using the same path string you defined in createRouteContract. Since both the router and useTypedParams reference AppRoutes, they are structurally linked.