Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,16 @@ The difference between `/:id*` and `/:id/*` is that in the former, the `id` para
- `/profile/:id/*`, with `/profile/123/abc`
- `id` is `123`

You can narrow prop types for your routes using `RoutePropsForPath<path>`:
```ts
import type { RoutePropsForPath } from 'preact-iso'

function User(props: RoutePropsForPath<'/user/:id'>) {
props.user.id2 // type error
props.user.id // no type error
}
```

### `useLocation`

A hook to work with the `LocationProvider` to access location context.
Expand Down
26 changes: 26 additions & 0 deletions src/router.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,32 @@ type RoutableProps =

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

export type RoutePropsForPath<Path extends string> = Path extends '*'
? { params: {}; rest: string }

: Path extends `:${infer placeholder}?/${infer rest}`
? { [k in placeholder]?: string } & { params: RoutePropsForPath<rest>['params'] & { [k in placeholder]?: string } } & Omit<RoutePropsForPath<rest>, 'params'>

: Path extends `:${infer placeholder}/${infer rest}`
? { [k in placeholder]: string } & { params: RoutePropsForPath<rest>['params'] & { [k in placeholder]: string } } & Omit<RoutePropsForPath<rest>, 'params'>

: Path extends `:${infer placeholder}?`
? { [k in placeholder]?: string } & { params: { [k in placeholder]?: string } }

: Path extends `:${infer placeholder}*`
? { [k in placeholder]?: string } & { params: { [k in placeholder]?: string } }

: Path extends `:${infer placeholder}+`
? { [k in placeholder]: string } & { params: { [k in placeholder]: string } }

: Path extends `:${infer placeholder}`
? { [k in placeholder]: string } & { params: { [k in placeholder]: string } }

: Path extends (`/${infer rest}` | `${infer _}/${infer rest}`)
? RoutePropsForPath<rest>

: { params: {} };

export function Route<Props>(props: RouteProps<Props> & Partial<Props>): VNode;

declare module 'preact' {
Expand Down
103 changes: 103 additions & 0 deletions test/node/pattern-match.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Test this file by running:
// npx tsc --noEmit test/node/pattern-match.types.ts

import type { RoutePropsForPath } from '../../src/router.js';

// Test utils

type isEqualsType<T, U> = T extends U ? U extends T ? true : false : false;
type isWeakEqualsType<T, U> = T extends U ? true : false;

// Type tests based on router-match.test.js cases

// Base route test
const test1: isEqualsType<
RoutePropsForPath<'/'> ,
{ params: {} }
> = true;

const test1_1: isEqualsType<
RoutePropsForPath<'/'> ,
{ arbitrary: {} }
> = false;

// Param route test
const test2: isEqualsType<
RoutePropsForPath<'/user/:id'> ,
{ params: { id: string }, id: string }
> = true;

const test2_weak: isWeakEqualsType<
RoutePropsForPath<'/user/:id'> ,
{ params: { id: string } }
> = true;

// Param rest segment test
const test3: isEqualsType<
RoutePropsForPath<'/user/*'> ,
{ params: {}, rest: string }
> = true;

const test3_1: isEqualsType<
RoutePropsForPath<'/*'> ,
{ params: {}, rest: string }
> = true;

const test3_2: isEqualsType<
RoutePropsForPath<'*'> ,
{ params: {}, rest: string }
> = true;

// Param route with rest segment test
const test4: isEqualsType<
RoutePropsForPath<'/user/:id/*'> ,
{ params: { id: string }, id: string, rest: string }
> = true;

// Optional param route test
const test5: isEqualsType<
RoutePropsForPath<'/user/:id?'> ,
{ params: { id?: string }, id?: string }
> = true;

// Optional rest param route "/:x*" test
const test6: isEqualsType<
RoutePropsForPath<'/user/:id*'> ,
{ params: { id?: string }, id?: string }
> = true;

// rest param should not be present
const test6_error: isEqualsType<
RoutePropsForPath<'/user/:id*'> ,
{ params: { id: string }, rest: string }
> = false;

// Rest param route "/:x+" test
const test7: isEqualsType<
RoutePropsForPath<'/user/:id+'> ,
{ params: { id: string }, id: string }
> = true;

// rest param should not be present
const test7_error: isEqualsType<
RoutePropsForPath<'/user/:id+'>,
{ params: { id: string }, id: string, rest: string }
> = false;

// Handles leading/trailing slashes test
const test8: isEqualsType<
RoutePropsForPath<'/about-late/:seg1/:seg2/'> ,
{ params: { seg1: string; seg2: string }, seg1: string, seg2: string }
> = true;

// Multiple params test (from overwrite properties test)
const test9: isEqualsType<
RoutePropsForPath<'/:path/:query'> ,
{ params: { path: string; query: string }, path: string, query: string }
> = true;

// Empty route test
const test10: isEqualsType<
RoutePropsForPath<''> ,
{ params: {} }
> = true;