diff --git a/contributors.yml b/contributors.yml index dafd53ea79..a5286cd163 100644 --- a/contributors.yml +++ b/contributors.yml @@ -70,6 +70,7 @@ - timdorr - tkindy - turansky +- tyankatsu0105 - underager - vijaypushkin - vikingviolinist diff --git a/docs/hooks/use-match.md b/docs/hooks/use-match.md index ddfa4111fb..6e6c8395d2 100644 --- a/docs/hooks/use-match.md +++ b/docs/hooks/use-match.md @@ -8,8 +8,8 @@ title: useMatch Type declaration ```tsx -declare function useMatch( - pattern: PathPattern | string +declare function useMatch, Path extends string>( + pattern: PathPattern | Path ): PathMatch | null; ``` diff --git a/docs/utils/generate-path.md b/docs/utils/generate-path.md index 1de10edfe9..465aee052e 100644 --- a/docs/utils/generate-path.md +++ b/docs/utils/generate-path.md @@ -8,9 +8,26 @@ title: generatePath Type declaration ```tsx -declare function generatePath( - path: string, - params?: Params + type PathParams< + Path extends string +> = Path extends `:${infer Param}/${infer Rest}` + ? Param | PathParams + : Path extends `:${infer Param}` + ? Param + : Path extends `${any}:${infer Param}` + ? PathParams<`:${Param}`> + : Path extends `${any}/*` + ? "*" + : Path extends "*" + ? "*" + : never + + +declare function generatePath( + path: Path, + params?: { + [key in PathParams]: string + } ): string; ``` diff --git a/packages/router/utils.ts b/packages/router/utils.ts index dad0bd8f76..974b10a966 100644 --- a/packages/router/utils.ts +++ b/packages/router/utils.ts @@ -159,51 +159,40 @@ export interface DataRouteObject extends RouteObject { id: string; } -type ParamParseFailed = { failed: true }; - -type ParamParseSegment = - // Check here if there exists a forward slash in the string. - Segment extends `${infer LeftSegment}/${infer RightSegment}` - ? // If there is a forward slash, then attempt to parse each side of the - // forward slash. - ParamParseSegment extends infer LeftResult - ? ParamParseSegment extends infer RightResult - ? LeftResult extends string - ? // If the left side is successfully parsed as a param, then check if - // the right side can be successfully parsed as well. If both sides - // can be parsed, then the result is a union of the two sides - // (read: "foo" | "bar"). - RightResult extends string - ? LeftResult | RightResult - : LeftResult - : // If the left side is not successfully parsed as a param, then check - // if only the right side can be successfully parse as a param. If it - // can, then the result is just right, else it's a failure. - RightResult extends string - ? RightResult - : ParamParseFailed - : ParamParseFailed - : // If the left side didn't parse into a param, then just check the right - // side. - ParamParseSegment extends infer RightResult - ? RightResult extends string - ? RightResult - : ParamParseFailed - : ParamParseFailed - : // If there's no forward slash, then check if this segment starts with a - // colon. If it does, then this is a dynamic segment, so the result is - // just the remainder of the string, optionally prefixed with another string. - // Otherwise, it's a failure. - Segment extends `${string}:${infer Remaining}` - ? Remaining - : ParamParseFailed; +type Star = "*" +/** + * @private + * Return string union from path string. + * @example + * PathParam<"/path/:a/:b"> // "a" | "b" + * PathParam<"/path/:a/:b/*"> // "a" | "b" | "*" + */ + type PathParam< + Path extends string +> = + // Check path string starts with slash and a param string. + Path extends `:${infer Param}/${infer Rest}` + ? Param | PathParam + // Check path string is a param string. + : Path extends `:${infer Param}` + ? Param + // Check path string ends with slash and a param string. + : Path extends `${any}/:${infer Param}` + ? PathParam<`:${Param}`> + // Check path string ends with slash and a star. + : Path extends `${any}/${Star}` + ? Star + // Check string is star. + : Path extends Star + ? Star + : never // Attempt to parse the given string segment. If it fails, then just return the // plain string type as a default fallback. Otherwise return the union of the // parsed string literals that were referenced as dynamic segments in the route. export type ParamParseKey = - ParamParseSegment extends string - ? ParamParseSegment + [PathParam] extends [never] + ? PathParam : string; /** @@ -446,14 +435,20 @@ function matchRouteBranch< * * @see https://reactrouter.com/docs/en/v6/utils/generate-path */ -export function generatePath(path: string, params: Params = {}): string { +export function generatePath(path: Path, params: { + [key in PathParam]: string +} = {} as any): string { return path - .replace(/:(\w+)/g, (_, key) => { + .replace(/:(\w+)/g, (_, key: PathParam) => { invariant(params[key] != null, `Missing ":${key}" param`); return params[key]!; }) .replace(/\/*\*$/, (_) => - params["*"] == null ? "" : params["*"].replace(/^\/*/, "/") + { + const star = "*" as PathParam + + return params[star] == null ? "" : params[star].replace(/^\/*/, "/") + } ); }