@@ -770,49 +770,63 @@ export type RouteManifest<R = DataRouteObject> = Record<string, R | undefined>;
770770
771771// prettier-ignore
772772type Regex_az = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z"
773- // prettier-ignore
774- type Regez_AZ = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z"
773+ type Regex_AZ = Uppercase < Regex_az > ;
775774type Regex_09 = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
776- type Regex_w = Regex_az | Regez_AZ | Regex_09 | "_" ;
777- type ParamChar = Regex_w | "-" ;
778-
779- // Emulates regex `+`
780- type RegexMatchPlus <
781- CharPattern extends string ,
782- T extends string ,
783- > = T extends `${infer First } ${infer Rest } `
784- ? First extends CharPattern
785- ? RegexMatchPlus < CharPattern , Rest > extends never
786- ? First
787- : `${ First } ${ RegexMatchPlus < CharPattern , Rest > } `
788- : never
789- : never ;
790-
791- // Recursive helper for finding path parameters in the absence of wildcards
792- type _PathParam < Path extends string > =
793- // split path into individual path segments
794- 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
802- never ;
803-
804- export type PathParam < Path extends string > =
775+ type Regex_w = Regex_az | Regex_AZ | Regex_09 | "_" ;
776+
777+ // prettier-ignore
778+ /** Emulates Regex `+` operator */
779+ type RegexMatchPlus < char extends string , T extends string > =
780+ _RegexMatchPlus < char , T > extends infer result extends string ?
781+ result extends '' ? never : result
782+ :
783+ never
784+
785+ // prettier-ignore
786+ type _RegexMatchPlus < char extends string , T extends string > =
787+ T extends `${infer head extends char } ${infer rest } ` ?
788+ `${ head } ${ _RegexMatchPlus < char , rest > } `
789+ :
790+ ''
791+
792+ type ParamNameChar = Regex_w | "-" ;
793+
794+ type Simplify < T > = { [ K in keyof T ] : T [ K ] } & { } ;
795+
796+ // prettier-ignore
797+ type GeneratePathParams < path extends string > = Simplify <
798+ & ParseParams < path >
799+ & { [ key in string ] : string | null | undefined }
800+ >
801+
802+ // prettier-ignore
803+ type ParseParams < path extends string > =
805804 // check if path is just a wildcard
806- Path extends "*" | "/*"
807- ? "*"
808- : // look for wildcard at the end of the path
809- Path extends `${infer Rest } /*`
810- ? "*" | _PathParam < Rest >
811- : // look for params in the absence of wildcards
812- _PathParam < Path > ;
805+ path extends '*' ? { '*' : string } :
806+ // look for wildcard at the end of the path
807+ path extends `${infer rest } /*` ? { '*' : string } & ParseParams < rest > :
808+ // look for params in the absence of wildcards
809+ _ParseParams < path > ;
810+
811+ // prettier-ignore
812+ type _ParseParams < path extends string > =
813+ // split path into individual path segments
814+ path extends `${infer left } /${infer right } ` ?
815+ _ParseParams < left > & _ParseParams < right > :
816+ // look for optional param in segment
817+ path extends `:${infer param } ?${string } ` ?
818+ { [ key in RegexMatchPlus < ParamNameChar , param > ] ?: string | null | undefined } :
819+ // look for required param in segment
820+ path extends `:${infer param } ` ?
821+ { [ key in RegexMatchPlus < ParamNameChar , param > ] : string } :
822+ { } ;
823+
824+ // prettier-ignore
825+ export type PathParam < path extends string > = ( keyof ParseParams < path > ) & string ;
813826
814827// eslint-disable-next-line @typescript-eslint/no-unused-vars
815828type _tests = [
829+ // PathParam
816830 Expect < Equal < PathParam < "/a/b/*" > , "*" > > ,
817831 Expect < Equal < PathParam < ":a" > , "a" > > ,
818832 Expect < Equal < PathParam < "/a/:b" > , "b" > > ,
@@ -821,6 +835,28 @@ type _tests = [
821835 Expect < Equal < PathParam < "/:a/b/:c/*" > , "a" | "c" | "*" > > ,
822836 Expect < Equal < PathParam < "/:lang.xml" > , "lang" > > ,
823837 Expect < Equal < PathParam < "/:lang?.xml" > , "lang" > > ,
838+
839+ // ParseParams
840+ Expect < Equal < ParseParams < "/a/b/*" > , { "*" : string } > > ,
841+ Expect < Equal < ParseParams < ":a" > , { a : string } > > ,
842+ Expect < Equal < ParseParams < "/a/:b" > , { b : string } > > ,
843+ Expect < Equal < ParseParams < "/a/blahblahblah:b" > , { } > > ,
844+ Expect < Equal < Simplify < ParseParams < "/:a/:b" > > , { a : string ; b : string } > > ,
845+ Expect <
846+ Equal <
847+ Simplify < ParseParams < "/:a/b/:c/*" > > ,
848+ { a : string ; c : string ; "*" : string }
849+ >
850+ > ,
851+ Expect < Equal < ParseParams < "/:lang.xml" > , { lang : string } > > ,
852+ Expect <
853+ Equal < ParseParams < "/:lang?.xml" > , { lang ?: string | null | undefined } >
854+ > ,
855+ Expect < Equal < Simplify < ParseParams < "/:a/:a" > > , { a : string } > > ,
856+ Expect < Equal < Simplify < ParseParams < "/:a/:a?" > > , { a : string } > > ,
857+ Expect <
858+ Equal < Simplify < ParseParams < "/:a?/:a?" > > , { a ?: string | null | undefined } >
859+ > ,
824860] ;
825861
826862// Attempt to parse the given string segment. If it fails, then just return the
@@ -1365,9 +1401,7 @@ function matchRouteBranch<
13651401 */
13661402export function generatePath < Path extends string > (
13671403 originalPath : Path ,
1368- params : {
1369- [ key in PathParam < Path > ] : string | null ;
1370- } = { } as any ,
1404+ params : GeneratePathParams < Path > = { } as any ,
13711405) : string {
13721406 let path : string = originalPath ;
13731407 if ( path . endsWith ( "*" ) && path !== "*" && ! path . endsWith ( "/*" ) ) {
@@ -1394,15 +1428,14 @@ export function generatePath<Path extends string>(
13941428
13951429 // only apply the splat if it's the last segment
13961430 if ( isLastSegment && segment === "*" ) {
1397- const star = "*" as PathParam < Path > ;
13981431 // Apply the splat
1399- return stringify ( params [ star ] ) ;
1432+ return stringify ( params [ "*" as keyof typeof params ] ) ;
14001433 }
14011434
14021435 const keyMatch = segment . match ( / ^ : ( [ \w - ] + ) ( \? ? ) ( .* ) / ) ;
14031436 if ( keyMatch ) {
14041437 const [ , key , optional , suffix ] = keyMatch ;
1405- let param = params [ key as PathParam < Path > ] ;
1438+ let param = params [ key as keyof typeof params ] ;
14061439 invariant ( optional === "?" || param != null , `Missing ":${ key } " param` ) ;
14071440 return encodeURIComponent ( stringify ( param ) ) + suffix ;
14081441 }
@@ -1477,13 +1510,10 @@ type Mutable<T> = {
14771510 * @returns A path match object if the pattern matches the pathname,
14781511 * or `null` if it does not match.
14791512 */
1480- export function matchPath <
1481- ParamKey extends ParamParseKey < Path > ,
1482- Path extends string ,
1483- > (
1513+ export function matchPath < Path extends string > (
14841514 pattern : PathPattern < Path > | Path ,
14851515 pathname : string ,
1486- ) : PathMatch < ParamKey > | null {
1516+ ) : PathMatch < ParamParseKey < Path > > | null {
14871517 if ( typeof pattern === "string" ) {
14881518 pattern = { path : pattern , caseSensitive : false , end : true } ;
14891519 }
0 commit comments