|
1 | 1 | import { RouteObject, createBrowserRouter } from 'react-router-dom'; |
2 | 2 | import { F } from 'ts-toolbelt'; |
3 | 3 |
|
| 4 | +type PathParams<T> = keyof T extends never ? { pathParams?: never } : { pathParams: T }; |
| 5 | + |
4 | 6 | type ExtractParam<Path, NextPart> = Path extends `:${infer Param}` ? Record<Param, string> & NextPart : NextPart; |
5 | 7 |
|
6 | 8 | type ExtractParams<Path> = Path extends `${infer Segment}/${infer Rest}` |
7 | 9 | ? ExtractParam<Segment, ExtractParams<Rest>> |
8 | 10 | : ExtractParam<Path, {}>; |
9 | 11 |
|
| 12 | +type PathWithParams<P extends string | undefined> = P extends string |
| 13 | + ? { path: P } & PathParams<ExtractParams<P>> |
| 14 | + : never; |
| 15 | + |
10 | 16 | type TypesafeRouteParams<Routes extends RouteObject[]> = { |
11 | | - [K in keyof Routes]: Routes[K] extends { path: string; children: RouteObject[] } |
12 | | - ? |
13 | | - | { path: Routes[K]['path']; pathParams: ExtractParams<Routes[K]['path']> } |
14 | | - | TypesafeRouteParams<Routes[K]['children']> |
15 | | - : Routes[K] extends { path: string } |
16 | | - ? { path: Routes[K]['path']; pathParams: ExtractParams<Routes[K]['path']> } |
17 | | - : never; |
| 17 | + [K in keyof Routes]: Routes[K] extends { children: infer C extends RouteObject[] } |
| 18 | + ? PathWithParams<Routes[K]['path']> | TypesafeRouteParams<C> |
| 19 | + : PathWithParams<Routes[K]['path']>; |
18 | 20 | }[number]; |
19 | 21 |
|
20 | 22 | type TypesafeSearchParams = Record<string, string> | URLSearchParams; |
21 | 23 | export type RouteExtraParams = { hash?: string; searchParams?: TypesafeSearchParams }; |
22 | 24 |
|
23 | | -function invariant(condition: any, message?: string): asserts condition { |
24 | | - if (condition) return; |
25 | | - throw new Error(message); |
26 | | -} |
27 | | - |
28 | 25 | const joinValidWith = |
29 | 26 | (separator: string) => |
30 | 27 | (...valid: any[]) => |
31 | 28 | valid.filter(Boolean).join(separator); |
32 | 29 |
|
33 | | -function assertPathAndParams(params: unknown): asserts params is { path: string; pathParams: Record<string, string> } { |
34 | | - invariant(params && typeof params === 'object' && 'path' in params && 'pathParams' in params); |
35 | | - invariant(typeof params.path === 'string'); |
36 | | - invariant(typeof params.pathParams === 'object' && params.pathParams !== null); |
37 | | -} |
38 | | - |
39 | 30 | export const typesafeBrowserRouter = <R extends RouteObject>(routes: F.Narrow<R[]>) => { |
40 | 31 | return { |
41 | 32 | router: createBrowserRouter(routes as RouteObject[]), |
42 | 33 | href: (route: TypesafeRouteParams<R[]> & RouteExtraParams) => { |
43 | | - assertPathAndParams(route); |
44 | | - let path = route.path; |
45 | 34 | // applies all params to the path |
46 | | - for (const param in route.pathParams) { |
47 | | - path = route.path.replace(`:${param}`, route.pathParams[param] as string); |
48 | | - } |
| 35 | + const path = route.pathParams |
| 36 | + ? Object.keys(route.pathParams).reduce((path, param) => { |
| 37 | + return path.replace(`:${param}`, route.pathParams![param as keyof typeof route.pathParams]); |
| 38 | + }, route.path) |
| 39 | + : route.path; |
| 40 | + |
49 | 41 | const searchParams = new URLSearchParams(route.searchParams); |
50 | 42 | const hash = route.hash?.replace(/^#/, ''); |
51 | 43 |
|
|
0 commit comments