For React Router apps without generated route types. Routes live in one map, and each path template gives you the type of its params. A typo in a key or a missing param is a compile error.

The map

export const ROUTES = {
  posts: "/blog/posts",
  post: "/blog/posts/:postId",
  postEdit: "/blog/posts/:postId/edit",
} as const;

type RouteKey = keyof typeof ROUTES;

Params from the template

type ParamsOf<P extends string> =
  P extends `${string}:${infer Name}/${infer Rest}` ? Name | ParamsOf<`/${Rest}`>
  : P extends `${string}:${infer Name}` ? Name
  : never;

type Params<K extends RouteKey> = Record<ParamsOf<(typeof ROUTES)[K]>, string>;
// Params<"postEdit">  →  { postId: string }

Helpers

export const to = <K extends RouteKey>(key: K, params: Params<K>) =>
  generatePath(ROUTES[key], params);

export const useRouteParams = <K extends RouteKey>(_key: K) =>
  useParams() as Params<K>;

Usage

<Route path={ROUTES.postEdit} element={<EditPost />} />

navigate(to("postEdit", { postId: "123" }));   // "/blog/posts/123/edit"
to("postEdit", {});                             // ✗ missing postId
to("postEdt", { postId: "1" });                 // ✗ no such route

const { postId } = useRouteParams("postEdit");  // string

On TanStack Router the generated route tree already gives you this, see Typed route path union.