Skip to content

Commit 8917a9d

Browse files
committed
fix: no longer require pathParams if not dynamic url
1 parent ee6b915 commit 8917a9d

File tree

3 files changed

+60
-23
lines changed

3 files changed

+60
-23
lines changed

.changeset/silly-baboons-film.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'react-router-typesafe': patch
3+
---
4+
5+
`typesafeBrowserRouter`: no longer requires `pathParams` to be passed as an empty object when route does not have dynamic segments
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { test, expect } from 'bun:test';
2+
import { typesafeBrowserRouter } from '../browser-router';
3+
4+
test('returns pathname with replaced params', () => {
5+
const { href } = typesafeBrowserRouter([
6+
{ path: '/blog', children: [{ path: '/blog/:postId', children: [{ path: '/blog/:postId/:commentId' }] }] },
7+
]);
8+
9+
const output = href({ path: '/blog/:postId/:commentId', pathParams: { postId: 'foo', commentId: 'bar' } });
10+
11+
expect(output).toEqual('/blog/foo/bar');
12+
});
13+
14+
test('returns pathname with search params if object is passed', () => {
15+
const { href } = typesafeBrowserRouter([{ path: '/blog', children: [{ path: '/blog/:postId' }] }]);
16+
17+
const output = href({ path: '/blog/:postId', pathParams: { postId: 'foo' }, searchParams: { foo: 'bar' } });
18+
19+
expect(output).toEqual('/blog/foo?foo=bar');
20+
});
21+
22+
test('returns pathname with search params if URLSearchParams is passed', () => {
23+
const { href } = typesafeBrowserRouter([{ path: '/blog', children: [{ path: '/blog/:postId' }] }]);
24+
25+
const output = href({
26+
path: '/blog/:postId',
27+
pathParams: { postId: 'foo' },
28+
searchParams: new URLSearchParams({ foo: 'bar' }),
29+
});
30+
31+
expect(output).toEqual('/blog/foo?foo=bar');
32+
});
33+
34+
test('returns pathname with hash', () => {
35+
const { href } = typesafeBrowserRouter([{ path: '/blog', children: [{ path: '/blog/:postId' }] }]);
36+
37+
const output = href({ path: '/blog/:postId', pathParams: { postId: 'foo' }, hash: '#foo' });
38+
39+
expect(output).toEqual('/blog/foo#foo');
40+
});

src/browser-router.ts

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,43 @@
11
import { RouteObject, createBrowserRouter } from 'react-router-dom';
22
import { F } from 'ts-toolbelt';
33

4+
type PathParams<T> = keyof T extends never ? { pathParams?: never } : { pathParams: T };
5+
46
type ExtractParam<Path, NextPart> = Path extends `:${infer Param}` ? Record<Param, string> & NextPart : NextPart;
57

68
type ExtractParams<Path> = Path extends `${infer Segment}/${infer Rest}`
79
? ExtractParam<Segment, ExtractParams<Rest>>
810
: ExtractParam<Path, {}>;
911

12+
type PathWithParams<P extends string | undefined> = P extends string
13+
? { path: P } & PathParams<ExtractParams<P>>
14+
: never;
15+
1016
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']>;
1820
}[number];
1921

2022
type TypesafeSearchParams = Record<string, string> | URLSearchParams;
2123
export type RouteExtraParams = { hash?: string; searchParams?: TypesafeSearchParams };
2224

23-
function invariant(condition: any, message?: string): asserts condition {
24-
if (condition) return;
25-
throw new Error(message);
26-
}
27-
2825
const joinValidWith =
2926
(separator: string) =>
3027
(...valid: any[]) =>
3128
valid.filter(Boolean).join(separator);
3229

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-
3930
export const typesafeBrowserRouter = <R extends RouteObject>(routes: F.Narrow<R[]>) => {
4031
return {
4132
router: createBrowserRouter(routes as RouteObject[]),
4233
href: (route: TypesafeRouteParams<R[]> & RouteExtraParams) => {
43-
assertPathAndParams(route);
44-
let path = route.path;
4534
// 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+
4941
const searchParams = new URLSearchParams(route.searchParams);
5042
const hash = route.hash?.replace(/^#/, '');
5143

0 commit comments

Comments
 (0)