diff --git a/contributors.yml b/contributors.yml index 21f580c381..fb7d2ecc0a 100644 --- a/contributors.yml +++ b/contributors.yml @@ -86,6 +86,7 @@ - dcblair - decadentsavant - developit +- devonpmack - dgrijuela - DigitalNaut - dmitrytarassov diff --git a/packages/react-router/lib/router/utils.ts b/packages/react-router/lib/router/utils.ts index cebea7406f..1a6e685584 100644 --- a/packages/react-router/lib/router/utils.ts +++ b/packages/react-router/lib/router/utils.ts @@ -555,7 +555,7 @@ type Regex_az = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" 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" type Regex_09 = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"; type Regex_w = Regex_az | Regez_AZ | Regex_09 | "_"; -type ParamChar = Regex_w | "-"; +type ParamChar = Regex_w | "-" | "|"; // Emulates regex `+` type RegexMatchPlus< @@ -1062,6 +1062,84 @@ function matchRouteBranch< return matches; } +type ExtractCharacters = T extends `${infer Left}${infer Right}` + ? Left | ExtractCharacters + : T; + +type OnlyIncludes = ExtractCharacters extends ExtractCharacters + ? true + : false; + +type IsExtractableRegex = OnlyIncludes, ParamChar> + +export type ExtractRegExpOptions = IsExtractableRegex extends true + ? ExtractOptions + : U; + +export type ExtractOptions = T extends `${infer Left}|${infer Right}` ? Left | ExtractOptions : T | null; + +export type ExtractRouteParam = + T extends `*` + ? { '*': U } + : T extends `${infer Param}*${infer _Rest}` + ? { [k in Param]: U } + : T extends `${infer Param}+${infer _Rest}` + ? { [k in Param]: U } + : T extends `${infer Param}?${infer _Rest}` + ? { [k in Param]: U } + : T extends `${infer Param}.${infer _Rest}` + ? { [k in Param]: U } + : { [k in T]: U }; + + +type Flatten = { + [P in keyof T]: T[P]; +}; + +export type ExtractRouteParams = Flatten> + +export type ExtractRouteParamsNonFlat = string extends T + ? { [k in string]: U } + : T extends `*` + ? { '*': U } + : T extends `:${infer ParamWithRegexp}/${infer Rest}` + ? ParamWithRegexp extends `${infer Param}(${infer RegExp})` + ? ExtractRouteParam> & ExtractRouteParams + : ExtractRouteParam & ExtractRouteParams + : T extends `:${infer ParamWithRegexp}` + ? ParamWithRegexp extends `${infer Param}(${infer RegExp})` + ? ExtractRouteParam> + : ExtractRouteParam + : T extends `${infer _Start}/:${infer ParamWithRegexp}/${infer Rest}` + ? ParamWithRegexp extends `${infer Param}(${infer RegExp})` + ? ExtractRouteParam> & ExtractRouteParams + : ExtractRouteParam & ExtractRouteParams + : T extends `${infer _Start}/:${infer ParamWithRegexp}` + ? ParamWithRegexp extends `${infer Param}(${infer RegExp})` + ? ExtractRouteParam> + : ExtractRouteParam + : T extends `${infer _Start}/*${infer Rest}` + ? { '*': U } & ExtractRouteParams + : {}; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type _testExtractRouteParams = [ + Expect, { postId: string | null }>>, + Expect + , { page: 'comments' | 'latest_activity' | null }>>, + Expect + , { page: string | null }>>, + Expect, { '*': string | null }>>, + Expect, { a: string | null }>>, + Expect, { a: string | null, b: string | null }>>, + Expect, { b: string | null }>>, + Expect, {}>>, + Expect, { a: string | null, b: string | null }>>, + Expect, { a: string | null, c: string | null, '*': string | null }>>, + Expect, { lang: string | null }>>, + Expect, { lang: string | null }>>, +]; + /** * Returns a path with params interpolated. * @@ -1069,9 +1147,7 @@ function matchRouteBranch< */ export function generatePath( originalPath: Path, - params: { - [key in PathParam]: string | null; - } = {} as any + params: ExtractRouteParams = {} as any ): string { let path: string = originalPath; if (path.endsWith("*") && path !== "*" && !path.endsWith("/*")) { @@ -1098,7 +1174,7 @@ export function generatePath( // only apply the splat if it's the last segment if (isLastSegment && segment === "*") { - const star = "*" as PathParam; + const star = "*" as keyof ExtractRouteParams; // Apply the splat return stringify(params[star]); } @@ -1106,7 +1182,7 @@ export function generatePath( const keyMatch = segment.match(/^:([\w-]+)(\??)$/); if (keyMatch) { const [, key, optional] = keyMatch; - let param = params[key as PathParam]; + let param = params[key as keyof ExtractRouteParams]; invariant(optional === "?" || param != null, `Missing ":${key}" param`); return stringify(param); }