Skip to content

Commit 5acd319

Browse files
authored
Make Params and related functions generic (#8019)
* Add generic for params type * Add undefined for param type * fix test types
1 parent 2e3c14c commit 5acd319

File tree

3 files changed

+50
-34
lines changed

3 files changed

+50
-34
lines changed

docs/api-reference.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -753,7 +753,7 @@ See also [`createRoutesFromArray`](#createroutesfromarray).
753753
<summary>Type declaration</summary>
754754

755755
```tsx
756-
declare function generatePath(path: string, params: Params = {}): string;
756+
declare function generatePath(path: string, params?: Params): string;
757757
```
758758

759759
</details>
@@ -813,10 +813,10 @@ This is the heart of React Router's matching algorithm. It is used internally by
813813
<summary>Type declaration</summary>
814814

815815
```tsx
816-
declare function matchPath(
816+
declare function matchPath<ParamKey extends string = string>(
817817
pattern: PathPattern,
818818
pathname: string
819-
): PathMatch | null;
819+
): PathMatch<ParamKey> | null;
820820

821821
type PathPattern =
822822
| string
@@ -1046,7 +1046,9 @@ function App() {
10461046
<summary>Type declaration</summary>
10471047

10481048
```tsx
1049-
declare function useMatch(pattern: PathPattern): PathMatch | null;
1049+
declare function useMatch<ParamKey extends string = string>(
1050+
pattern: PathPattern
1051+
): PathMatch<ParamKey> | null;
10501052
```
10511053

10521054
</details>
@@ -1119,7 +1121,7 @@ Returns the element for the child route at this level of the route hierarchy. Th
11191121
<summary>Type declaration</summary>
11201122

11211123
```tsx
1122-
declare function useParams(): Params;
1124+
declare function useParams<Key extends string = string>(): Params<Key>;
11231125
```
11241126

11251127
</details>

packages/react-router/__tests__/useParams-test.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
describe("useParams", () => {
1212
describe("when the route isn't matched", () => {
1313
it("returns an empty object", () => {
14-
let params: Record<string, string> = {};
14+
let params: Record<string, string | undefined> = {};
1515
function Home() {
1616
params = useParams();
1717
return null;
@@ -30,7 +30,7 @@ describe("useParams", () => {
3030

3131
describe("when the path has no params", () => {
3232
it("returns an empty object", () => {
33-
let params: Record<string, string> = {};
33+
let params: Record<string, string | undefined> = {};
3434
function Home() {
3535
params = useParams();
3636
return null;
@@ -51,7 +51,7 @@ describe("useParams", () => {
5151

5252
describe("when the path has some params", () => {
5353
it("returns an object of the URL params", () => {
54-
let params: Record<string, string> = {};
54+
let params: Record<string, string | undefined> = {};
5555
function BlogPost() {
5656
params = useParams();
5757
return null;
@@ -73,7 +73,7 @@ describe("useParams", () => {
7373

7474
describe("a child route", () => {
7575
it("returns a combined hash of the parent and child params", () => {
76-
let params: Record<string, string> = {};
76+
let params: Record<string, string | undefined> = {};
7777

7878
function Course() {
7979
params = useParams();
@@ -110,7 +110,7 @@ describe("useParams", () => {
110110

111111
describe("when the path has percent-encoded params", () => {
112112
it("returns an object of the decoded params", () => {
113-
let params: Record<string, string> = {};
113+
let params: Record<string, string | undefined> = {};
114114
function BlogPost() {
115115
params = useParams();
116116
return null;
@@ -133,7 +133,7 @@ describe("useParams", () => {
133133

134134
describe("when the path has a + character", () => {
135135
it("returns an object of the decoded params", () => {
136-
let params: Record<string, string> = {};
136+
let params: Record<string, string | undefined> = {};
137137
function BlogPost() {
138138
params = useParams();
139139
return null;
@@ -169,7 +169,7 @@ describe("useParams", () => {
169169
});
170170

171171
it("returns the raw value and warns", () => {
172-
let params: Record<string, string> = {};
172+
let params: Record<string, string | undefined> = {};
173173
function BlogPost() {
174174
params = useParams();
175175
return null;
@@ -196,7 +196,7 @@ describe("useParams", () => {
196196

197197
describe("when the params match in a child route", () => {
198198
it("renders params in the parent", () => {
199-
let params: Record<string, string> = {};
199+
let params: Record<string, string | undefined> = {};
200200
function Blog() {
201201
params = useParams();
202202
return <h1>{params.slug}</h1>;

packages/react-router/index.tsx

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import type {
1212
Transition
1313
} from "history";
1414

15+
type Mutable<T> = {
16+
-readonly [P in keyof T]: T[P];
17+
};
18+
1519
const readOnly: <T>(obj: T) => Readonly<T> = __DEV__
1620
? obj => Object.freeze(obj)
1721
: obj => obj;
@@ -87,9 +91,9 @@ const RouteContext = React.createContext<RouteContextObject>({
8791
route: null
8892
});
8993

90-
interface RouteContextObject {
94+
interface RouteContextObject<ParamKey extends string = string> {
9195
outlet: React.ReactElement | null;
92-
params: Params;
96+
params: Params<ParamKey>;
9397
pathname: string;
9498
basename: string;
9599
route: RouteObject | null;
@@ -374,7 +378,9 @@ export function useLocation(): Location {
374378
*
375379
* @see https://reactrouter.com/api/useMatch
376380
*/
377-
export function useMatch(pattern: PathPattern): PathMatch | null {
381+
export function useMatch<ParamKey extends string = string>(
382+
pattern: PathPattern
383+
): PathMatch<ParamKey> | null {
378384
invariant(
379385
useInRouterContext(),
380386
// TODO: This error is probably because they somehow have 2 versions of the
@@ -383,7 +389,7 @@ export function useMatch(pattern: PathPattern): PathMatch | null {
383389
);
384390

385391
let location = useLocation() as Location;
386-
return matchPath(pattern, location.pathname);
392+
return matchPath<ParamKey>(pattern, location.pathname);
387393
}
388394

389395
type PathPattern =
@@ -468,7 +474,7 @@ export function useOutlet(): React.ReactElement | null {
468474
*
469475
* @see https://reactrouter.com/api/useParams
470476
*/
471-
export function useParams(): Params {
477+
export function useParams<Key extends string = string>(): Params<Key> {
472478
return React.useContext(RouteContext).params;
473479
}
474480

@@ -666,7 +672,9 @@ export function createRoutesFromChildren(
666672
/**
667673
* The parameters that were parsed from the URL path.
668674
*/
669-
export type Params = Record<string, string>;
675+
export type Params<Key extends string = string> = {
676+
readonly [key in Key]: string | undefined;
677+
};
670678

671679
/**
672680
* A route object represents a logical route, with (optionally) its child
@@ -700,7 +708,7 @@ export function generatePath(path: string, params: Params = {}): string {
700708
return path
701709
.replace(/:(\w+)/g, (_, key) => {
702710
invariant(params[key] != null, `Missing ":${key}" param`);
703-
return params[key];
711+
return params[key]!;
704712
})
705713
.replace(/\/*\*$/, _ =>
706714
params["*"] == null ? "" : params["*"].replace(/^\/*/, "/")
@@ -750,10 +758,10 @@ export function matchRoutes(
750758
return matches;
751759
}
752760

753-
export interface RouteMatch {
761+
export interface RouteMatch<ParamKey extends string = string> {
754762
route: RouteObject;
755763
pathname: string;
756-
params: Params;
764+
params: Params<ParamKey>;
757765
}
758766

759767
function flattenRoutes(
@@ -861,13 +869,13 @@ function stableSort(array: any[], compareItems: (a: any, b: any) => number) {
861869
array.sort((a, b) => compareItems(a, b) || copy.indexOf(a) - copy.indexOf(b));
862870
}
863871

864-
function matchRouteBranch(
872+
function matchRouteBranch<ParamKey extends string = string>(
865873
branch: RouteBranch,
866874
pathname: string
867-
): RouteMatch[] | null {
875+
): RouteMatch<ParamKey>[] | null {
868876
let routes = branch[1];
869877
let matchedPathname = "/";
870-
let matchedParams: Params = {};
878+
let matchedParams = {} as Params<ParamKey>;
871879

872880
let matches: RouteMatch[] = [];
873881
for (let i = 0; i < routes.length; ++i) {
@@ -893,7 +901,7 @@ function matchRouteBranch(
893901
matches.push({
894902
route,
895903
pathname: matchedPathname,
896-
params: readOnly<Params>(matchedParams)
904+
params: readOnly<Params<ParamKey>>(matchedParams)
897905
});
898906
}
899907

@@ -906,10 +914,10 @@ function matchRouteBranch(
906914
*
907915
* @see https://reactrouter.com/api/matchPath
908916
*/
909-
export function matchPath(
917+
export function matchPath<ParamKey extends string = string>(
910918
pattern: PathPattern,
911919
pathname: string
912-
): PathMatch | null {
920+
): PathMatch<ParamKey> | null {
913921
if (typeof pattern === "string") {
914922
pattern = { path: pattern };
915923
}
@@ -922,18 +930,24 @@ export function matchPath(
922930

923931
let matchedPathname = match[1];
924932
let values = match.slice(2);
925-
let params = paramNames.reduce((memo, paramName, index) => {
926-
memo[paramName] = safelyDecodeURIComponent(values[index] || "", paramName);
927-
return memo;
928-
}, {} as Params);
933+
let params: Params = paramNames.reduce<Mutable<Params>>(
934+
(memo, paramName, index) => {
935+
memo[paramName] = safelyDecodeURIComponent(
936+
values[index] || "",
937+
paramName
938+
);
939+
return memo;
940+
},
941+
{}
942+
);
929943

930944
return { path, pathname: matchedPathname, params };
931945
}
932946

933-
export interface PathMatch {
947+
export interface PathMatch<ParamKey extends string = string> {
934948
path: string;
935949
pathname: string;
936-
params: Params;
950+
params: Params<ParamKey>;
937951
}
938952

939953
function compilePath(

0 commit comments

Comments
 (0)