Skip to content

Commit ba9fb3a

Browse files
committed
Fix generatePath typing for optional params
Differentiate required and optional path params so generatePath accepts omitted optional params without weakening required ones.
1 parent 4547c80 commit ba9fb3a

File tree

2 files changed

+73
-13
lines changed

2 files changed

+73
-13
lines changed

.changeset/fresh-bears-drive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
Enhance `generatePath` types to better differentiate between optional and required params

packages/react-router/lib/router/utils.ts

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -788,28 +788,55 @@ type RegexMatchPlus<
788788
: never
789789
: never;
790790

791-
// Recursive helper for finding path parameters in the absence of wildcards
792-
type _PathParam<Path extends string> =
791+
// Union of all path parameters - required and optional
792+
export type PathParam<Path extends string> =
793+
| RequiredPathParam<Path>
794+
| OptionalPathParam<Path>;
795+
796+
// Recursive helper for finding required path parameters in the absence of wildcards
797+
type _RequiredPathParam<Path extends string> =
793798
// split path into individual path segments
794799
Path extends `${infer L}/${infer R}`
795-
? _PathParam<L> | _PathParam<R>
796-
: // find params after `:`
797-
Path extends `:${infer Param}`
798-
? Param extends `${infer Optional}?${string}`
799-
? RegexMatchPlus<ParamChar, Optional>
800-
: RegexMatchPlus<ParamChar, Param>
801-
: // otherwise, there aren't any params present
800+
? _RequiredPathParam<L> | _RequiredPathParam<R>
801+
: // ignore optional params
802+
Path extends `:${string}?${string}`
803+
? never
804+
: // find required params after `:`
805+
Path extends `:${infer Param}`
806+
? RegexMatchPlus<ParamChar, Param>
807+
: // otherwise, there aren't any params present
808+
never;
809+
810+
type RequiredPathParam<Path extends string> =
811+
// check if path is just a wildcard
812+
Path extends "*" | "/*"
813+
? "*"
814+
: // look for wildcard at the end of the path
815+
Path extends `${infer Rest}/*`
816+
? "*" | _RequiredPathParam<Rest>
817+
: // look for params in the absence of wildcards
818+
_RequiredPathParam<Path>;
819+
820+
// Recursive helper for finding optional path parameters in the absence of wildcards
821+
type _OptionalPathParam<Path extends string> =
822+
// split path into individual path segments
823+
Path extends `${infer L}/${infer R}`
824+
? _OptionalPathParam<L> | _OptionalPathParam<R>
825+
: // find optional params after `:`
826+
Path extends `:${infer Optional}?${infer Rest}`
827+
? RegexMatchPlus<ParamChar, Optional> | _OptionalPathParam<Rest>
828+
: // otherwise, there aren't any optional params present
802829
never;
803830

804-
export type PathParam<Path extends string> =
831+
type OptionalPathParam<Path extends string> =
805832
// check if path is just a wildcard
806833
Path extends "*" | "/*"
807834
? "*"
808835
: // look for wildcard at the end of the path
809836
Path extends `${infer Rest}/*`
810-
? "*" | _PathParam<Rest>
837+
? "*" | _OptionalPathParam<Rest>
811838
: // look for params in the absence of wildcards
812-
_PathParam<Path>;
839+
_OptionalPathParam<Path>;
813840

814841
// eslint-disable-next-line @typescript-eslint/no-unused-vars
815842
type _tests = [
@@ -821,6 +848,32 @@ type _tests = [
821848
Expect<Equal<PathParam<"/:a/b/:c/*">, "a" | "c" | "*">>,
822849
Expect<Equal<PathParam<"/:lang.xml">, "lang">>,
823850
Expect<Equal<PathParam<"/:lang?.xml">, "lang">>,
851+
Expect<Equal<RequiredPathParam<"/a/b/*">, "*">>,
852+
Expect<Equal<RequiredPathParam<":a">, "a">>,
853+
Expect<Equal<RequiredPathParam<"/a/:b">, "b">>,
854+
Expect<Equal<RequiredPathParam<"/a/blahblahblah:b">, never>>,
855+
Expect<Equal<RequiredPathParam<"/:a/:b">, "a" | "b">>,
856+
Expect<Equal<RequiredPathParam<"/:a/b/:c/*">, "a" | "c" | "*">>,
857+
Expect<Equal<RequiredPathParam<"/:lang.xml">, "lang">>,
858+
Expect<Equal<OptionalPathParam<"/a/b/*">, "*">>,
859+
Expect<Equal<OptionalPathParam<":a?">, "a">>,
860+
Expect<Equal<OptionalPathParam<"/a/:b?">, "b">>,
861+
Expect<Equal<OptionalPathParam<"/a/blahblahblah:b?">, never>>,
862+
Expect<Equal<OptionalPathParam<"/:a?/:b?">, "a" | "b">>,
863+
Expect<Equal<OptionalPathParam<"/:a?/b/:c?/*">, "a" | "c" | "*">>,
864+
Expect<Equal<OptionalPathParam<"/:lang?.xml">, "lang">>,
865+
Expect<Equal<PathParam<"/:a?/:b">, "a" | "b">>,
866+
Expect<Equal<RequiredPathParam<"/:a?/:b">, "b">>,
867+
Expect<Equal<OptionalPathParam<"/:a?/:b">, "a">>,
868+
Expect<Equal<PathParam<"/:a?/b/:c">, "a" | "c">>,
869+
Expect<Equal<RequiredPathParam<"/:a?/b/:c">, "c">>,
870+
Expect<Equal<OptionalPathParam<"/:a?/b/:c">, "a">>,
871+
Expect<Equal<PathParam<"/:a/:b">, "a" | "b">>,
872+
Expect<Equal<RequiredPathParam<"/:a/:b">, "a" | "b">>,
873+
Expect<Equal<OptionalPathParam<"/:a/:b">, never>>,
874+
Expect<Equal<PathParam<"/:a?/:b?">, "a" | "b">>,
875+
Expect<Equal<RequiredPathParam<"/:a?/:b?">, never>>,
876+
Expect<Equal<OptionalPathParam<"/:a?/:b?">, "a" | "b">>,
824877
];
825878

826879
// Attempt to parse the given string segment. If it fails, then just return the
@@ -1366,7 +1419,9 @@ function matchRouteBranch<
13661419
export function generatePath<Path extends string>(
13671420
originalPath: Path,
13681421
params: {
1369-
[key in PathParam<Path>]: string | null;
1422+
[key in RequiredPathParam<Path>]: string;
1423+
} & {
1424+
[key in OptionalPathParam<Path>]?: string | null | undefined;
13701425
} = {} as any,
13711426
): string {
13721427
let path: string = originalPath;

0 commit comments

Comments
 (0)