Skip to content

Commit 578cc0b

Browse files
authored
Merge pull request #9090 from tyankatsu0105/feat/imp-generatePath-type
feat: improve generatePath arg type
2 parents c3406eb + e6f0f64 commit 578cc0b

File tree

4 files changed

+61
-48
lines changed

4 files changed

+61
-48
lines changed

contributors.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
- timdorr
7171
- tkindy
7272
- turansky
73+
- tyankatsu0105
7374
- underager
7475
- vijaypushkin
7576
- vikingviolinist

docs/hooks/use-match.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ title: useMatch
88
<summary>Type declaration</summary>
99

1010
```tsx
11-
declare function useMatch<ParamKey extends string = string>(
12-
pattern: PathPattern | string
11+
declare function useMatch<ParamKey extends ParamParseKey<Path>, Path extends string>(
12+
pattern: PathPattern<Path> | Path
1313
): PathMatch<ParamKey> | null;
1414
```
1515

docs/utils/generate-path.md

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,26 @@ title: generatePath
88
<summary>Type declaration</summary>
99

1010
```tsx
11-
declare function generatePath(
12-
path: string,
13-
params?: Params
11+
type PathParams<
12+
Path extends string
13+
> = Path extends `:${infer Param}/${infer Rest}`
14+
? Param | PathParams<Rest>
15+
: Path extends `:${infer Param}`
16+
? Param
17+
: Path extends `${any}:${infer Param}`
18+
? PathParams<`:${Param}`>
19+
: Path extends `${any}/*`
20+
? "*"
21+
: Path extends "*"
22+
? "*"
23+
: never
24+
25+
26+
declare function generatePath<Path extends string>(
27+
path: Path,
28+
params?: {
29+
[key in PathParams<Path>]: string
30+
}
1431
): string;
1532
```
1633

packages/router/utils.ts

Lines changed: 38 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -161,51 +161,40 @@ export interface DataRouteObject extends RouteObject {
161161
id: string;
162162
}
163163

164-
type ParamParseFailed = { failed: true };
165-
166-
type ParamParseSegment<Segment extends string> =
167-
// Check here if there exists a forward slash in the string.
168-
Segment extends `${infer LeftSegment}/${infer RightSegment}`
169-
? // If there is a forward slash, then attempt to parse each side of the
170-
// forward slash.
171-
ParamParseSegment<LeftSegment> extends infer LeftResult
172-
? ParamParseSegment<RightSegment> extends infer RightResult
173-
? LeftResult extends string
174-
? // If the left side is successfully parsed as a param, then check if
175-
// the right side can be successfully parsed as well. If both sides
176-
// can be parsed, then the result is a union of the two sides
177-
// (read: "foo" | "bar").
178-
RightResult extends string
179-
? LeftResult | RightResult
180-
: LeftResult
181-
: // If the left side is not successfully parsed as a param, then check
182-
// if only the right side can be successfully parse as a param. If it
183-
// can, then the result is just right, else it's a failure.
184-
RightResult extends string
185-
? RightResult
186-
: ParamParseFailed
187-
: ParamParseFailed
188-
: // If the left side didn't parse into a param, then just check the right
189-
// side.
190-
ParamParseSegment<RightSegment> extends infer RightResult
191-
? RightResult extends string
192-
? RightResult
193-
: ParamParseFailed
194-
: ParamParseFailed
195-
: // If there's no forward slash, then check if this segment starts with a
196-
// colon. If it does, then this is a dynamic segment, so the result is
197-
// just the remainder of the string, optionally prefixed with another string.
198-
// Otherwise, it's a failure.
199-
Segment extends `${string}:${infer Remaining}`
200-
? Remaining
201-
: ParamParseFailed;
164+
type Star = "*"
165+
/**
166+
* @private
167+
* Return string union from path string.
168+
* @example
169+
* PathParam<"/path/:a/:b"> // "a" | "b"
170+
* PathParam<"/path/:a/:b/*"> // "a" | "b" | "*"
171+
*/
172+
type PathParam<
173+
Path extends string
174+
> =
175+
// Check path string starts with slash and a param string.
176+
Path extends `:${infer Param}/${infer Rest}`
177+
? Param | PathParam<Rest>
178+
// Check path string is a param string.
179+
: Path extends `:${infer Param}`
180+
? Param
181+
// Check path string ends with slash and a param string.
182+
: Path extends `${any}/:${infer Param}`
183+
? PathParam<`:${Param}`>
184+
// Check path string ends with slash and a star.
185+
: Path extends `${any}/${Star}`
186+
? Star
187+
// Check string is star.
188+
: Path extends Star
189+
? Star
190+
: never
202191

203192
// Attempt to parse the given string segment. If it fails, then just return the
204193
// plain string type as a default fallback. Otherwise return the union of the
205194
// parsed string literals that were referenced as dynamic segments in the route.
206195
export type ParamParseKey<Segment extends string> =
207-
ParamParseSegment<Segment> extends string
208-
? ParamParseSegment<Segment>
196+
[PathParam<Segment>] extends [never]
197+
? PathParam<Segment>
209198
: string;
210199

211200
/**
@@ -475,14 +464,20 @@ function matchRouteBranch<
475464
*
476465
* @see https://reactrouter.com/docs/en/v6/utils/generate-path
477466
*/
478-
export function generatePath(path: string, params: Params = {}): string {
467+
export function generatePath<Path extends string>(path: Path, params: {
468+
[key in PathParam<Path>]: string
469+
} = {} as any): string {
479470
return path
480-
.replace(/:(\w+)/g, (_, key) => {
471+
.replace(/:(\w+)/g, (_, key: PathParam<Path>) => {
481472
invariant(params[key] != null, `Missing ":${key}" param`);
482473
return params[key]!;
483474
})
484475
.replace(/\/*\*$/, (_) =>
485-
params["*"] == null ? "" : params["*"].replace(/^\/*/, "/")
476+
{
477+
const star = "*" as PathParam<Path>
478+
479+
return params[star] == null ? "" : params[star].replace(/^\/*/, "/")
480+
}
486481
);
487482
}
488483

0 commit comments

Comments
 (0)