diff --git a/README.md b/README.md index 01e6b48..0be0e9c 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,12 @@ useRoute("/app*"); // optional wildcards, matches "/orders", "/orders/" // and "/orders/completed/list" useRoute("/orders/*?"); + +// regex for matching complex patterns, +// matches "/hello:123" +useRoute(/^[/]([a-z]+):([0-9]+)[/]?$/); +// and with named capture groups +useRoute(/^[/](?[a-z]+):(?[0-9]+)[/]?$/); ``` The second item in the pair `params` is an object with parameters or null if there was no match. For wildcard segments the parameter name is `"*"`: @@ -312,11 +318,29 @@ const User = () => { const params = useParams(); params.id; // "1" + + // alternatively, use the index to access the prop + params[0]; // "1" }; /> ``` +It is the same for regex paths. Capture groups can be accessed by their index, or if there is a named capture group, that can be used instead. + +```js +import { Route, useParams } from "wouter"; + +const User = () => { + const params = useParams(); + + params.id; // "1" + params[0]; // "1" +}; + +[0-9]+)[/]?$/} component={User}> /> +``` + ### `useSearch`: query strings Use this hook to get the current search (query) string value. It will cause your component to re-render only when the string itself and not the full location updates. The search string returned **does not** contain a `?` character. @@ -421,6 +445,11 @@ If you call `useLocation()` inside the last route, it will return `/orders` and ``` +**Note:** The `nest` prop does not alter the regex passed into regex paths. +Instead, the `nest` prop will only determine if nested routes will match against the rest of path or the same path. +To make a strict path regex, use a regex pattern like `/^[/](your pattern)[/]?$/` (this matches an optional end slash and the end of the string). +To make a nestable regex, use a regex pattern like `/^[/](your pattern)(?=$|[/])/` (this matches either the end of the string or a slash for future segments). + ### `` Link component renders an `` element that, when clicked, performs a navigation. diff --git a/package-lock.json b/package-lock.json index 18ba9b3..b3732d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6499,7 +6499,7 @@ } }, "packages/wouter": { - "version": "3.1.2", + "version": "3.1.3", "license": "Unlicense", "dependencies": { "mitt": "^3.0.1", @@ -6511,7 +6511,7 @@ } }, "packages/wouter-preact": { - "version": "3.1.2", + "version": "3.1.3", "license": "Unlicense", "dependencies": { "mitt": "^3.0.1", diff --git a/package.json b/package.json index 9bf90a4..ccde718 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "packages/wouter-preact" ], "scripts": { - "fix:p": "prettier --write './**/*.(js|ts){x,}'", + "fix:p": "prettier --write \"./**/*.(js|ts){x,}\"", "test": "vitest", "size": "size-limit", "build": "npm run build -ws", diff --git a/packages/wouter-preact/types/index.d.ts b/packages/wouter-preact/types/index.d.ts index 99f5328..aa5030b 100644 --- a/packages/wouter-preact/types/index.d.ts +++ b/packages/wouter-preact/types/index.d.ts @@ -10,6 +10,7 @@ import { import { Path, + PathPattern, BaseLocationHook, HookReturnValue, HookNavigationOptions, @@ -29,11 +30,16 @@ export * from "./router.js"; import { RouteParams } from "regexparam"; +export type StringRouteParams = RouteParams & { + [param: number]: string | undefined; +}; +export type RegexRouteParams = { [key: string | number]: string | undefined }; + /** * Route patterns and parameters */ export interface DefaultParams { - readonly [paramName: string]: string | undefined; + readonly [paramName: string | number]: string | undefined; } export type Params = T; @@ -57,23 +63,33 @@ export interface RouteComponentProps { export interface RouteProps< T extends DefaultParams | undefined = undefined, - RoutePath extends Path = Path + RoutePath extends PathPattern = PathPattern > { children?: | (( - params: T extends DefaultParams ? T : RouteParams + params: T extends DefaultParams + ? T + : RoutePath extends string + ? StringRouteParams + : RegexRouteParams ) => ComponentChildren) | ComponentChildren; path?: RoutePath; component?: ComponentType< - RouteComponentProps> + RouteComponentProps< + T extends DefaultParams + ? T + : RoutePath extends string + ? StringRouteParams + : RegexRouteParams + > >; nest?: boolean; } export function Route< T extends DefaultParams | undefined = undefined, - RoutePath extends Path = Path + RoutePath extends PathPattern = PathPattern >(props: RouteProps): ReturnType; /* @@ -143,10 +159,16 @@ export function useRouter(): RouterObject; export function useRoute< T extends DefaultParams | undefined = undefined, - RoutePath extends Path = Path + RoutePath extends PathPattern = PathPattern >( pattern: RoutePath -): Match>; +): Match< + T extends DefaultParams + ? T + : RoutePath extends string + ? StringRouteParams + : RegexRouteParams +>; export function useLocation< H extends BaseLocationHook = BrowserLocationHook @@ -157,7 +179,7 @@ export function useSearch< >(): ReturnType; export function useParams(): T extends string - ? RouteParams + ? StringRouteParams : T extends undefined ? DefaultParams : T; diff --git a/packages/wouter-preact/types/location-hook.d.ts b/packages/wouter-preact/types/location-hook.d.ts index ab3ca90..202ccb2 100644 --- a/packages/wouter-preact/types/location-hook.d.ts +++ b/packages/wouter-preact/types/location-hook.d.ts @@ -4,6 +4,8 @@ export type Path = string; +export type PathPattern = string | RegExp; + export type SearchString = string; // the base useLocation hook type. Any custom hook (including the diff --git a/packages/wouter-preact/types/router.d.ts b/packages/wouter-preact/types/router.d.ts index d0c1ee2..370e206 100644 --- a/packages/wouter-preact/types/router.d.ts +++ b/packages/wouter-preact/types/router.d.ts @@ -5,7 +5,10 @@ import { BaseSearchHook, } from "./location-hook.js"; -export type Parser = (route: Path) => { pattern: RegExp; keys: string[] }; +export type Parser = ( + route: Path, + loose?: boolean +) => { pattern: RegExp; keys: string[] }; export type HrefsFormatter = (href: string, router: RouterObject) => string; diff --git a/packages/wouter/src/index.js b/packages/wouter/src/index.js index 9012798..b5b28ce 100644 --- a/packages/wouter/src/index.js +++ b/packages/wouter/src/index.js @@ -79,21 +79,45 @@ export const useSearch = () => { }; const matchRoute = (parser, route, path, loose) => { + // if the input is a regexp, skip parsing + const { pattern, keys } = + route instanceof RegExp + ? { keys: false, pattern: route } + : parser(route || "*", loose); + + // array destructuring loses keys, so this is done in two steps + const result = pattern.exec(path) || []; + // when parser is in "loose" mode, `$base` is equal to the // first part of the route that matches the pattern // (e.g. for pattern `/a/:b` and path `/a/1/2/3` the `$base` is `a/1`) // we use this for route nesting - const { pattern, keys } = parser(route || "*", loose); - const [$base, ...matches] = pattern.exec(path) || []; + const [$base, ...matches] = result; return $base !== undefined ? [ true, - // an object with parameters matched, e.g. { foo: "bar" } for "/:foo" - // we "zip" two arrays here to construct the object - // ["foo"], ["bar"] → { foo: "bar" } - Object.fromEntries(keys.map((key, i) => [key, matches[i]])), + (() => { + // for regex paths, `keys` will always be false + + // an object with parameters matched, e.g. { foo: "bar" } for "/:foo" + // we "zip" two arrays here to construct the object + // ["foo"], ["bar"] → { foo: "bar" } + const groups = + keys !== false + ? Object.fromEntries(keys.map((key, i) => [key, matches[i]])) + : result.groups; + + // convert the array to an instance of object + // this makes it easier to integrate with the existing param implementation + let obj = { ...matches }; + + // merge named capture groups with matches array + groups && Object.assign(obj, groups); + + return obj; + })(), // the third value if only present when parser is in "loose" mode, // so that we can extract the base path for nested routes diff --git a/packages/wouter/test/parser.test.tsx b/packages/wouter/test/parser.test.tsx index a2cbf62..8b595dd 100644 --- a/packages/wouter/test/parser.test.tsx +++ b/packages/wouter/test/parser.test.tsx @@ -42,6 +42,6 @@ it("allows to change the behaviour of route matching", () => { expect(result.current).toStrictEqual([ true, - { pages: undefined, rest: "10/bio", 0: "home" }, + { 0: "home", 1: undefined, 2: "10/bio", pages: undefined, rest: "10/bio" }, ]); }); diff --git a/packages/wouter/test/route.test-d.tsx b/packages/wouter/test/route.test-d.tsx index b34fc4f..6e83db9 100644 --- a/packages/wouter/test/route.test-d.tsx +++ b/packages/wouter/test/route.test-d.tsx @@ -8,9 +8,9 @@ describe("`path` prop", () => { assertType(); }); - it("should be a string", () => { + it("should be a string or RegExp", () => { let a: ComponentProps["path"]; - expectTypeOf(a).toMatchTypeOf(); + expectTypeOf(a).toMatchTypeOf(); }); }); diff --git a/packages/wouter/test/route.test.tsx b/packages/wouter/test/route.test.tsx index d83cc47..d80039b 100644 --- a/packages/wouter/test/route.test.tsx +++ b/packages/wouter/test/route.test.tsx @@ -95,3 +95,47 @@ it("supports `base` routers with relative path", () => { unmount(); }); + +it("supports `path` prop with regex", () => { + const result = testRouteRender( + "/foo", + +

Hello!

+
+ ); + + expect(result.findByType("h1").props.children).toBe("Hello!"); +}); + +it("supports regex path named params", () => { + const result = testRouteRender( + "/users/alex", + [a-z]+)/}> + {(params) =>

{params.name}

} +
+ ); + + expect(result.findByType("h1").props.children).toBe("alex"); +}); + +it("supports regex path anonymous params", () => { + const result = testRouteRender( + "/users/alex", + + {(params) =>

{params[0]}

} +
+ ); + + expect(result.findByType("h1").props.children).toBe("alex"); +}); + +it("rejects when a path does not match the regex", () => { + const result = testRouteRender( + "/users/1234", + [a-z]+)/}> + {(params) =>

{params.name}

} +
+ ); + + expect(() => result.findByType("h1")).toThrow(); +}); diff --git a/packages/wouter/test/router.test-d.tsx b/packages/wouter/test/router.test-d.tsx index 6604554..790f735 100644 --- a/packages/wouter/test/router.test-d.tsx +++ b/packages/wouter/test/router.test-d.tsx @@ -1,6 +1,13 @@ import { ComponentProps } from "react"; import { it, expectTypeOf } from "vitest"; -import { Router, Route, BaseLocationHook, useRouter } from "wouter"; +import { + Router, + Route, + BaseLocationHook, + useRouter, + Parser, + Path, +} from "wouter"; it("should have at least one child", () => { // @ts-expect-error @@ -64,6 +71,17 @@ it("accepts `hrefs` function for transforming href strings", () => { ; }); +it("accepts `parser` function for generating regular expressions", () => { + const parser: Parser = (path: Path, loose?: boolean) => { + return { + pattern: new RegExp(`^${path}${loose === true ? "(?=$|[/])" : "[/]$"}`), + keys: [], + }; + }; + + this is a valid router; +}); + it("does not accept other props", () => { const router = useRouter(); diff --git a/packages/wouter/test/use-params.test-d.ts b/packages/wouter/test/use-params.test-d.ts index 29d4de1..972c379 100644 --- a/packages/wouter/test/use-params.test-d.ts +++ b/packages/wouter/test/use-params.test-d.ts @@ -10,12 +10,18 @@ it("returns an object with arbitrary parameters", () => { expectTypeOf(params).toBeObject(); expectTypeOf(params.any).toEqualTypeOf(); + expectTypeOf(params[0]).toEqualTypeOf(); }); it("can infer the type of parameters from the route path", () => { const params = useParams<"/app/users/:name?/:id">(); - expectTypeOf(params).toMatchTypeOf<{ id: string; name?: string }>(); + expectTypeOf(params).toMatchTypeOf<{ + 0?: string; + 1?: string; + id: string; + name?: string; + }>(); }); it("can accept the custom type of parameters as a generic argument", () => { diff --git a/packages/wouter/test/use-params.test.tsx b/packages/wouter/test/use-params.test.tsx index 7528ce1..409d6d7 100644 --- a/packages/wouter/test/use-params.test.tsx +++ b/packages/wouter/test/use-params.test.tsx @@ -18,7 +18,10 @@ it("contains a * parameter when used inside an empty ", () => { ), }); - expect(result.current).toEqual({ "*": "app-2/goods/tees" }); + expect(result.current).toEqual({ + 0: "app-2/goods/tees", + "*": "app-2/goods/tees", + }); }); it("returns an empty object when there are no params", () => { @@ -40,7 +43,12 @@ it("returns parameters from the closest parent match", () => { ), }); - expect(result.current).toEqual({ id: "1", name: "maria" }); + expect(result.current).toEqual({ + 0: "1", + 1: "maria", + id: "1", + name: "maria", + }); }); it("rerenders with parameters change", () => { @@ -57,10 +65,20 @@ it("rerenders with parameters change", () => { expect(result.current).toBeNull(); act(() => navigate("/posts/all")); - expect(result.current).toEqual({ a: "posts", b: "all" }); + expect(result.current).toEqual({ + 0: "posts", + 1: "all", + a: "posts", + b: "all", + }); act(() => navigate("/posts/latest")); - expect(result.current).toEqual({ a: "posts", b: "latest" }); + expect(result.current).toEqual({ + 0: "posts", + 1: "latest", + a: "posts", + b: "latest", + }); }); it("extracts parameters of the nested route", () => { @@ -79,5 +97,10 @@ it("extracts parameters of the nested route", () => { ), }); - expect(result.current).toEqual({ version: "v2", chain: "eth" }); + expect(result.current).toEqual({ + 0: "v2", + 1: "eth", + version: "v2", + chain: "eth", + }); }); diff --git a/packages/wouter/test/use-route.test-d.ts b/packages/wouter/test/use-route.test-d.ts index 9836f7e..8638ea8 100644 --- a/packages/wouter/test/use-route.test-d.ts +++ b/packages/wouter/test/use-route.test-d.ts @@ -40,6 +40,9 @@ it("infers parameters from the route path", () => { if (inferedParams) { expectTypeOf(inferedParams).toMatchTypeOf<{ + 0?: string; + 1?: string; + 2?: string; name?: string; id: string; wildcard?: string; diff --git a/packages/wouter/test/use-route.test.tsx b/packages/wouter/test/use-route.test.tsx index 0e300de..61e3a85 100644 --- a/packages/wouter/test/use-route.test.tsx +++ b/packages/wouter/test/use-route.test.tsx @@ -1,26 +1,29 @@ import { renderHook, act } from "@testing-library/react"; -import { useRoute, Match, Router } from "wouter"; +import { useRoute, Match, Router, RegexRouteParams } from "wouter"; import { it, expect } from "vitest"; import { memoryLocation } from "wouter/memory-location"; it("is case insensitive", () => { assertRoute("/Users", "/users", {}); assertRoute("/HomePage", "/Homepage", {}); - assertRoute("/Users/:Name", "/users/alex", { Name: "alex" }); + assertRoute("/Users/:Name", "/users/alex", { 0: "alex", Name: "alex" }); }); it("supports required segments", () => { - assertRoute("/:page", "/users", { page: "users" }); + assertRoute("/:page", "/users", { 0: "users", page: "users" }); assertRoute("/:page", "/users/all", false); - assertRoute("/:page", "/1", { page: "1" }); + assertRoute("/:page", "/1", { 0: "1", page: "1" }); - assertRoute("/home/:page/etc", "/home/users/etc", { page: "users" }); + assertRoute("/home/:page/etc", "/home/users/etc", { + 0: "users", + page: "users", + }); assertRoute("/home/:page/etc", "/home/etc", false); assertRoute( "/root/payments/:id/refunds/:refId", "/root/payments/1/refunds/2", - [true, { id: "1", refId: "2" }] + [true, { 0: "1", 1: "2", id: "1", refId: "2" }] ); }); @@ -31,22 +34,41 @@ it("ignores the trailing slash", () => { assertRoute("/home/", "/home/", {}); assertRoute("/home/", "/home", {}); - assertRoute("/:page", "/users/", [true, { page: "users" }]); - assertRoute("/catalog/:section?", "/catalog/", { section: undefined }); + assertRoute("/:page", "/users/", [true, { 0: "users", page: "users" }]); + assertRoute("/catalog/:section?", "/catalog/", { + 0: undefined, + section: undefined, + }); }); it("supports trailing wildcards", () => { - assertRoute("/app/*", "/app/", { "*": "" }); - assertRoute("/app/*", "/app/dashboard/intro", { "*": "dashboard/intro" }); - assertRoute("/app/*", "/app/charges/1", { "*": "charges/1" }); + assertRoute("/app/*", "/app/", { 0: "", "*": "" }); + assertRoute("/app/*", "/app/dashboard/intro", { + 0: "dashboard/intro", + "*": "dashboard/intro", + }); + assertRoute("/app/*", "/app/charges/1", { 0: "charges/1", "*": "charges/1" }); }); it("supports wildcards in the middle of the pattern", () => { - assertRoute("/app/*/settings", "/app/users/settings", { "*": "users" }); - assertRoute("/app/*/settings", "/app/users/1/settings", { "*": "users/1" }); + assertRoute("/app/*/settings", "/app/users/settings", { + 0: "users", + "*": "users", + }); + assertRoute("/app/*/settings", "/app/users/1/settings", { + 0: "users/1", + "*": "users/1", + }); - assertRoute("/*/payments/:id", "/home/payments/1", { "*": "home", id: "1" }); + assertRoute("/*/payments/:id", "/home/payments/1", { + 0: "home", + 1: "1", + "*": "home", + id: "1", + }); assertRoute("/*/payments/:id?", "/home/payments", { + 0: "home", + 1: undefined, "*": "home", id: undefined, }); @@ -54,60 +76,90 @@ it("supports wildcards in the middle of the pattern", () => { it("uses a question mark to define optional segments", () => { assertRoute("/books/:genre/:title?", "/books/scifi", { + 0: "scifi", + 1: undefined, genre: "scifi", title: undefined, }); assertRoute("/books/:genre/:title?", "/books/scifi/dune", { + 0: "scifi", + 1: "dune", genre: "scifi", title: "dune", }); assertRoute("/books/:genre/:title?", "/books/scifi/dune/all", false); assertRoute("/app/:company?/blog/:post", "/app/apple/blog/mac", { + 0: "apple", + 1: "mac", company: "apple", post: "mac", }); assertRoute("/app/:company?/blog/:post", "/app/blog/mac", { + 0: undefined, + 1: "mac", company: undefined, post: "mac", }); }); it("supports optional wildcards", () => { - assertRoute("/app/*?", "/app/blog/mac", { "*": "blog/mac" }); - assertRoute("/app/*?", "/app", { "*": undefined }); - assertRoute("/app/*?/dashboard", "/app/v1/dashboard", { "*": "v1" }); - assertRoute("/app/*?/dashboard", "/app/dashboard", { "*": undefined }); + assertRoute("/app/*?", "/app/blog/mac", { 0: "blog/mac", "*": "blog/mac" }); + assertRoute("/app/*?", "/app", { 0: undefined, "*": undefined }); + assertRoute("/app/*?/dashboard", "/app/v1/dashboard", { 0: "v1", "*": "v1" }); + assertRoute("/app/*?/dashboard", "/app/dashboard", { + 0: undefined, + "*": undefined, + }); assertRoute("/app/*?/users/:name", "/app/users/karen", { + 0: undefined, + 1: "karen", "*": undefined, name: "karen", }); }); it("supports other characters in segments", () => { - assertRoute("/users/:name", "/users/1-alex", { name: "1-alex" }); + assertRoute("/users/:name", "/users/1-alex", { 0: "1-alex", name: "1-alex" }); assertRoute("/staff/:name/:bio?", "/staff/John Doe 3", { + 0: "John Doe 3", + 1: undefined, name: "John Doe 3", bio: undefined, }); assertRoute("/staff/:name/:bio?", "/staff/John Doe 3/bio", { + 0: "John Doe 3", + 1: "bio", name: "John Doe 3", bio: "bio", }); assertRoute("/users/:name/bio", "/users/$102_Kathrine&/bio", { + 0: "$102_Kathrine&", name: "$102_Kathrine&", }); }); it("ignores escaped slashes", () => { - assertRoute("/:param/bar", "/foo%2Fbar/bar", { param: "foo%2Fbar" }); + assertRoute("/:param/bar", "/foo%2Fbar/bar", { + 0: "foo%2Fbar", + param: "foo%2Fbar", + }); assertRoute("/:param", "/foo%2Fbar%D1%81%D0%B0%D0%BD%D1%8F", { + 0: "foo%2Fbarсаня", param: "foo%2Fbarсаня", }); }); +it("supports regex patterns", () => { + assertRoute(/[/]foo/, "/foo", {}); + assertRoute(/[/]([a-z]+)/, "/bar", { 0: "bar" }); + assertRoute(/[/]([a-z]+)/, "/123", false); + assertRoute(/[/](?[a-z]+)/, "/bar", { 0: "bar", param: "bar" }); + assertRoute(/[/](?[a-z]+)/, "/123", false); +}); + it("reacts to pattern updates", () => { const { result, rerender } = renderHook( ({ pattern }: { pattern: string }) => useRoute(pattern), @@ -130,17 +182,27 @@ it("reacts to pattern updates", () => { rerender({ pattern: "/blog/:category/:post/:action" }); expect(result.current).toStrictEqual([ true, - { category: "products", post: "40", action: "read-all" }, + { + 0: "products", + 1: "40", + 2: "read-all", + category: "products", + post: "40", + action: "read-all", + }, ]); rerender({ pattern: "/blog/products/:id?/read-all" }); - expect(result.current).toStrictEqual([true, { id: "40" }]); + expect(result.current).toStrictEqual([true, { 0: "40", id: "40" }]); rerender({ pattern: "/blog/products/:name" }); expect(result.current).toStrictEqual([false, null]); rerender({ pattern: "/blog/*" }); - expect(result.current).toStrictEqual([true, { "*": "products/40/read-all" }]); + expect(result.current).toStrictEqual([ + true, + { 0: "products/40/read-all", "*": "products/40/read-all" }, + ]); }); it("reacts to location updates", () => { @@ -153,16 +215,19 @@ it("reacts to location updates", () => { expect(result.current).toStrictEqual([false, null]); act(() => navigate("/cities/berlin")); - expect(result.current).toStrictEqual([true, { city: "berlin" }]); + expect(result.current).toStrictEqual([true, { 0: "berlin", city: "berlin" }]); act(() => navigate("/cities/Tokyo")); - expect(result.current).toStrictEqual([true, { city: "Tokyo" }]); + expect(result.current).toStrictEqual([true, { 0: "Tokyo", city: "Tokyo" }]); act(() => navigate("/about")); expect(result.current).toStrictEqual([false, null]); act(() => navigate("/cities")); - expect(result.current).toStrictEqual([true, { city: undefined }]); + expect(result.current).toStrictEqual([ + true, + { 0: undefined, city: undefined }, + ]); }); /** @@ -170,7 +235,7 @@ it("reacts to location updates", () => { */ const assertRoute = ( - pattern: string, + pattern: string | RegExp, location: string, rhs: false | Match | Record ) => { diff --git a/packages/wouter/types/index.d.ts b/packages/wouter/types/index.d.ts index 4e9fc8e..449c133 100644 --- a/packages/wouter/types/index.d.ts +++ b/packages/wouter/types/index.d.ts @@ -14,6 +14,7 @@ import { import { Path, + PathPattern, BaseLocationHook, HookReturnValue, HookNavigationOptions, @@ -33,11 +34,16 @@ export * from "./router.js"; import { RouteParams } from "regexparam"; +export type StringRouteParams = RouteParams & { + [param: number]: string | undefined; +}; +export type RegexRouteParams = { [key: string | number]: string | undefined }; + /** * Route patterns and parameters */ export interface DefaultParams { - readonly [paramName: string]: string | undefined; + readonly [paramName: string | number]: string | undefined; } export type Params = T; @@ -61,23 +67,33 @@ export interface RouteComponentProps { export interface RouteProps< T extends DefaultParams | undefined = undefined, - RoutePath extends Path = Path + RoutePath extends PathPattern = PathPattern > { children?: | (( - params: T extends DefaultParams ? T : RouteParams + params: T extends DefaultParams + ? T + : RoutePath extends string + ? StringRouteParams + : RegexRouteParams ) => ReactNode) | ReactNode; path?: RoutePath; component?: ComponentType< - RouteComponentProps> + RouteComponentProps< + T extends DefaultParams + ? T + : RoutePath extends string + ? StringRouteParams + : RegexRouteParams + > >; nest?: boolean; } export function Route< T extends DefaultParams | undefined = undefined, - RoutePath extends Path = Path + RoutePath extends PathPattern = PathPattern >(props: RouteProps): ReturnType; /* @@ -150,10 +166,16 @@ export function useRouter(): RouterObject; export function useRoute< T extends DefaultParams | undefined = undefined, - RoutePath extends Path = Path + RoutePath extends PathPattern = PathPattern >( pattern: RoutePath -): Match>; +): Match< + T extends DefaultParams + ? T + : RoutePath extends string + ? StringRouteParams + : RegexRouteParams +>; export function useLocation< H extends BaseLocationHook = BrowserLocationHook @@ -164,7 +186,7 @@ export function useSearch< >(): ReturnType; export function useParams(): T extends string - ? RouteParams + ? StringRouteParams : T extends undefined ? DefaultParams : T; diff --git a/packages/wouter/types/location-hook.d.ts b/packages/wouter/types/location-hook.d.ts index 9bcfc3c..0d1aac1 100644 --- a/packages/wouter/types/location-hook.d.ts +++ b/packages/wouter/types/location-hook.d.ts @@ -4,6 +4,8 @@ export type Path = string; +export type PathPattern = string | RegExp; + export type SearchString = string; // the base useLocation hook type. Any custom hook (including the diff --git a/packages/wouter/types/router.d.ts b/packages/wouter/types/router.d.ts index d0c1ee2..370e206 100644 --- a/packages/wouter/types/router.d.ts +++ b/packages/wouter/types/router.d.ts @@ -5,7 +5,10 @@ import { BaseSearchHook, } from "./location-hook.js"; -export type Parser = (route: Path) => { pattern: RegExp; keys: string[] }; +export type Parser = ( + route: Path, + loose?: boolean +) => { pattern: RegExp; keys: string[] }; export type HrefsFormatter = (href: string, router: RouterObject) => string;