Skip to content

Commit 0549fd5

Browse files
authored
Helper type to narrow route props (#121)
* added pattern match type * move the type and add to README * review changes
1 parent 5577ed1 commit 0549fd5

File tree

3 files changed

+139
-0
lines changed

3 files changed

+139
-0
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,16 @@ The difference between `/:id*` and `/:id/*` is that in the former, the `id` para
261261
- `/profile/:id/*`, with `/profile/123/abc`
262262
- `id` is `123`
263263

264+
You can narrow prop types for your routes using `RoutePropsForPath<path>`:
265+
```ts
266+
import type { RoutePropsForPath } from 'preact-iso'
267+
268+
function User(props: RoutePropsForPath<'/user/:id'>) {
269+
props.user.id2 // type error
270+
props.user.id // no type error
271+
}
272+
```
273+
264274
### `useLocation`
265275

266276
A hook to work with the `LocationProvider` to access location context.

src/router.d.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,32 @@ type RoutableProps =
5959

6060
export type RouteProps<Props> = RoutableProps & { component: AnyComponent<Props> };
6161

62+
export type RoutePropsForPath<Path extends string> = Path extends '*'
63+
? { params: {}; rest: string }
64+
65+
: Path extends `:${infer placeholder}?/${infer rest}`
66+
? { [k in placeholder]?: string } & { params: RoutePropsForPath<rest>['params'] & { [k in placeholder]?: string } } & Omit<RoutePropsForPath<rest>, 'params'>
67+
68+
: Path extends `:${infer placeholder}/${infer rest}`
69+
? { [k in placeholder]: string } & { params: RoutePropsForPath<rest>['params'] & { [k in placeholder]: string } } & Omit<RoutePropsForPath<rest>, 'params'>
70+
71+
: Path extends `:${infer placeholder}?`
72+
? { [k in placeholder]?: string } & { params: { [k in placeholder]?: string } }
73+
74+
: Path extends `:${infer placeholder}*`
75+
? { [k in placeholder]?: string } & { params: { [k in placeholder]?: string } }
76+
77+
: Path extends `:${infer placeholder}+`
78+
? { [k in placeholder]: string } & { params: { [k in placeholder]: string } }
79+
80+
: Path extends `:${infer placeholder}`
81+
? { [k in placeholder]: string } & { params: { [k in placeholder]: string } }
82+
83+
: Path extends (`/${infer rest}` | `${infer _}/${infer rest}`)
84+
? RoutePropsForPath<rest>
85+
86+
: { params: {} };
87+
6288
export function Route<Props>(props: RouteProps<Props> & Partial<Props>): VNode;
6389

6490
declare module 'preact' {

test/node/pattern-match.types.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Test this file by running:
2+
// npx tsc --noEmit test/node/pattern-match.types.ts
3+
4+
import type { RoutePropsForPath } from '../../src/router.js';
5+
6+
// Test utils
7+
8+
type isEqualsType<T, U> = T extends U ? U extends T ? true : false : false;
9+
type isWeakEqualsType<T, U> = T extends U ? true : false;
10+
11+
// Type tests based on router-match.test.js cases
12+
13+
// Base route test
14+
const test1: isEqualsType<
15+
RoutePropsForPath<'/'> ,
16+
{ params: {} }
17+
> = true;
18+
19+
const test1_1: isEqualsType<
20+
RoutePropsForPath<'/'> ,
21+
{ arbitrary: {} }
22+
> = false;
23+
24+
// Param route test
25+
const test2: isEqualsType<
26+
RoutePropsForPath<'/user/:id'> ,
27+
{ params: { id: string }, id: string }
28+
> = true;
29+
30+
const test2_weak: isWeakEqualsType<
31+
RoutePropsForPath<'/user/:id'> ,
32+
{ params: { id: string } }
33+
> = true;
34+
35+
// Param rest segment test
36+
const test3: isEqualsType<
37+
RoutePropsForPath<'/user/*'> ,
38+
{ params: {}, rest: string }
39+
> = true;
40+
41+
const test3_1: isEqualsType<
42+
RoutePropsForPath<'/*'> ,
43+
{ params: {}, rest: string }
44+
> = true;
45+
46+
const test3_2: isEqualsType<
47+
RoutePropsForPath<'*'> ,
48+
{ params: {}, rest: string }
49+
> = true;
50+
51+
// Param route with rest segment test
52+
const test4: isEqualsType<
53+
RoutePropsForPath<'/user/:id/*'> ,
54+
{ params: { id: string }, id: string, rest: string }
55+
> = true;
56+
57+
// Optional param route test
58+
const test5: isEqualsType<
59+
RoutePropsForPath<'/user/:id?'> ,
60+
{ params: { id?: string }, id?: string }
61+
> = true;
62+
63+
// Optional rest param route "/:x*" test
64+
const test6: isEqualsType<
65+
RoutePropsForPath<'/user/:id*'> ,
66+
{ params: { id?: string }, id?: string }
67+
> = true;
68+
69+
// rest param should not be present
70+
const test6_error: isEqualsType<
71+
RoutePropsForPath<'/user/:id*'> ,
72+
{ params: { id: string }, rest: string }
73+
> = false;
74+
75+
// Rest param route "/:x+" test
76+
const test7: isEqualsType<
77+
RoutePropsForPath<'/user/:id+'> ,
78+
{ params: { id: string }, id: string }
79+
> = true;
80+
81+
// rest param should not be present
82+
const test7_error: isEqualsType<
83+
RoutePropsForPath<'/user/:id+'>,
84+
{ params: { id: string }, id: string, rest: string }
85+
> = false;
86+
87+
// Handles leading/trailing slashes test
88+
const test8: isEqualsType<
89+
RoutePropsForPath<'/about-late/:seg1/:seg2/'> ,
90+
{ params: { seg1: string; seg2: string }, seg1: string, seg2: string }
91+
> = true;
92+
93+
// Multiple params test (from overwrite properties test)
94+
const test9: isEqualsType<
95+
RoutePropsForPath<'/:path/:query'> ,
96+
{ params: { path: string; query: string }, path: string, query: string }
97+
> = true;
98+
99+
// Empty route test
100+
const test10: isEqualsType<
101+
RoutePropsForPath<''> ,
102+
{ params: {} }
103+
> = true;

0 commit comments

Comments
 (0)