Skip to content

Commit 540f94b

Browse files
brophdawg11GuptaSiddhantsiddhant-gupta-accenture
authored
fix: Strengthen route typings (#9366)
* Add conditional type to RouteObject - RouteObject type should accept either index or children. * Add contributor * Enhance children if index is falsy * Throw error in createRoutesFromChildren + test * Update error message * fix: Stengthen route typings top to bottom * Add test+invariant for index/path combinations * Remove duplicate check * Add invariant for index/path to flattenRoutes * Make children checks consistent * Allow index + path routes * remove data router example changes * Remove createroutesFromChildren test * Fix error message * List type props * add changeset Co-authored-by: Siddhant Gupta <[email protected]> Co-authored-by: Siddhant Gupta <[email protected]>
1 parent 434003d commit 540f94b

File tree

13 files changed

+196
-60
lines changed

13 files changed

+196
-60
lines changed

.changeset/swift-beers-sell.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"react-router": patch
3+
"react-router-dom": patch
4+
"react-router-native": patch
5+
"@remix-run/router": patch
6+
---
7+
8+
fix: Strengthen RouteObject/RouteProps types and throw on index routes with children (#9366)

contributors.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
- goldins
3737
- gowthamvbhat
3838
- GraxMonzo
39+
- GuptaSiddhant
3940
- haivuw
4041
- hernanif1
4142
- hongji00

packages/react-router-dom/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export type {
8080
DataRouteObject,
8181
Fetcher,
8282
Hash,
83+
IndexRouteObject,
8384
IndexRouteProps,
8485
JsonFunction,
8586
LayoutRouteProps,
@@ -92,6 +93,7 @@ export type {
9293
NavigateProps,
9394
Navigation,
9495
Navigator,
96+
NonIndexRouteObject,
9597
OutletProps,
9698
Params,
9799
ParamParseKey,

packages/react-router-native/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export type {
2727
DataRouteObject,
2828
Fetcher,
2929
Hash,
30+
IndexRouteObject,
3031
IndexRouteProps,
3132
JsonFunction,
3233
LayoutRouteProps,
@@ -39,6 +40,7 @@ export type {
3940
NavigateProps,
4041
Navigation,
4142
Navigator,
43+
NonIndexRouteObject,
4244
OutletProps,
4345
Params,
4446
ParamParseKey,

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,4 +196,17 @@ describe("creating routes from JSX", () => {
196196
]
197197
`);
198198
});
199+
200+
it("throws when the index route has children", () => {
201+
expect(() => {
202+
createRoutesFromChildren(
203+
<Route path="/">
204+
{/* @ts-expect-error */}
205+
<Route index>
206+
<Route path="users" />
207+
</Route>
208+
</Route>
209+
);
210+
}).toThrow("An index route cannot have child routes.");
211+
});
199212
});

packages/react-router/__tests__/index-routes-test.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ describe("index route matching", () => {
88
{
99
path: "/users",
1010
children: [
11+
// This config is not valid because index routes cannot have children
12+
// @ts-expect-error
1113
{
1214
index: true,
13-
// This config is not valid because index routes cannot have children
1415
children: [{ path: "not-valid" }],
1516
},
1617
{ path: ":id" },
@@ -19,6 +20,8 @@ describe("index route matching", () => {
1920
],
2021
"/users/mj"
2122
);
22-
}).toThrow("must not have child routes");
23+
}).toThrowErrorMatchingInlineSnapshot(
24+
`"Index routes must not have child routes. Please remove all child routes from route path \\"/users/\\"."`
25+
);
2326
});
2427
});

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { AgnosticRouteObject } from "@remix-run/router";
12
import * as React from "react";
23
import type { RouteObject } from "react-router";
34
import { matchRoutes } from "react-router";
@@ -43,8 +44,7 @@ describe("matchRoutes", () => {
4344
{ path: "*", element: <h1>Not Found</h1> },
4445
],
4546
};
46-
47-
let routes = [
47+
let routes: RouteObject[] = [
4848
{ path: "/", element: <h1>Root Layout</h1> },
4949
{
5050
path: "/home",
@@ -71,7 +71,7 @@ describe("matchRoutes", () => {
7171
});
7272

7373
it("matches index routes with path over layout", () => {
74-
expect(matchRoutes(routes, "/layout")[0].route.index).toBe(true);
74+
expect(matchRoutes(routes, "/layout")?.[0].route.index).toBe(true);
7575
expect(pickPaths(routes, "/layout")).toEqual(["/layout"]);
7676
});
7777

packages/react-router/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,10 @@ import {
6464
import type {
6565
DataRouteMatch,
6666
DataRouteObject,
67+
IndexRouteObject,
6768
Navigator,
6869
NavigateOptions,
70+
NonIndexRouteObject,
6971
RouteMatch,
7072
RouteObject,
7173
RelativeRoutingType,
@@ -116,6 +118,7 @@ export type {
116118
DataRouteObject,
117119
Fetcher,
118120
Hash,
121+
IndexRouteObject,
119122
IndexRouteProps,
120123
JsonFunction,
121124
LayoutRouteProps,
@@ -128,6 +131,7 @@ export type {
128131
NavigateProps,
129132
Navigation,
130133
Navigator,
134+
NonIndexRouteObject,
131135
OutletProps,
132136
Params,
133137
ParamParseKey,

packages/react-router/lib/components.tsx

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as React from "react";
22
import type {
33
TrackedPromise,
4-
HydrationState,
54
InitialEntry,
65
Location,
76
MemoryHistory,
@@ -22,9 +21,11 @@ import { useSyncExternalStore as useSyncExternalStoreShim } from "./use-sync-ext
2221

2322
import type {
2423
DataRouteObject,
24+
IndexRouteObject,
2525
RouteMatch,
2626
RouteObject,
2727
Navigator,
28+
NonIndexRouteObject,
2829
RelativeRoutingType,
2930
} from "./context";
3031
import {
@@ -220,49 +221,46 @@ export function Outlet(props: OutletProps): React.ReactElement | null {
220221
return useOutlet(props.context);
221222
}
222223

223-
interface DataRouteProps {
224-
id?: RouteObject["id"];
225-
loader?: RouteObject["loader"];
226-
action?: RouteObject["action"];
227-
errorElement?: RouteObject["errorElement"];
228-
shouldRevalidate?: RouteObject["shouldRevalidate"];
229-
handle?: RouteObject["handle"];
230-
}
231-
232-
export interface RouteProps extends DataRouteProps {
233-
caseSensitive?: boolean;
234-
children?: React.ReactNode;
235-
element?: React.ReactNode | null;
236-
index?: boolean;
237-
path?: string;
238-
}
239-
240-
export interface PathRouteProps extends DataRouteProps {
241-
caseSensitive?: boolean;
242-
children?: React.ReactNode;
243-
element?: React.ReactNode | null;
224+
export interface PathRouteProps {
225+
caseSensitive?: NonIndexRouteObject["caseSensitive"];
226+
path?: NonIndexRouteObject["path"];
227+
id?: NonIndexRouteObject["id"];
228+
loader?: NonIndexRouteObject["loader"];
229+
action?: NonIndexRouteObject["action"];
230+
hasErrorBoundary?: NonIndexRouteObject["hasErrorBoundary"];
231+
shouldRevalidate?: NonIndexRouteObject["shouldRevalidate"];
232+
handle?: NonIndexRouteObject["handle"];
244233
index?: false;
245-
path: string;
246-
}
247-
248-
export interface LayoutRouteProps extends DataRouteProps {
249234
children?: React.ReactNode;
250235
element?: React.ReactNode | null;
236+
errorElement?: React.ReactNode | null;
251237
}
252238

253-
export interface IndexRouteProps extends DataRouteProps {
254-
element?: React.ReactNode | null;
239+
export interface LayoutRouteProps extends PathRouteProps {}
240+
241+
export interface IndexRouteProps {
242+
caseSensitive?: IndexRouteObject["caseSensitive"];
243+
path?: IndexRouteObject["path"];
244+
id?: IndexRouteObject["id"];
245+
loader?: IndexRouteObject["loader"];
246+
action?: IndexRouteObject["action"];
247+
hasErrorBoundary?: IndexRouteObject["hasErrorBoundary"];
248+
shouldRevalidate?: IndexRouteObject["shouldRevalidate"];
249+
handle?: IndexRouteObject["handle"];
255250
index: true;
251+
children?: undefined;
252+
element?: React.ReactNode | null;
253+
errorElement?: React.ReactNode | null;
256254
}
257255

256+
export type RouteProps = PathRouteProps | LayoutRouteProps | IndexRouteProps;
257+
258258
/**
259259
* Declares an element that should be rendered at a certain URL path.
260260
*
261261
* @see https://reactrouter.com/docs/en/v6/components/route
262262
*/
263-
export function Route(
264-
_props: PathRouteProps | LayoutRouteProps | IndexRouteProps
265-
): React.ReactElement | null {
263+
export function Route(_props: RouteProps): React.ReactElement | null {
266264
invariant(
267265
false,
268266
`A <Route> is only ever to be used as the child of <Routes> element, ` +
@@ -569,6 +567,11 @@ export function createRoutesFromChildren(
569567
}] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>`
570568
);
571569

570+
invariant(
571+
!element.props.index || !element.props.children,
572+
"An index route cannot have child routes."
573+
);
574+
572575
let treePath = [...parentPath, index];
573576
let route: RouteObject = {
574577
id: element.props.id || treePath.join("-"),

packages/react-router/lib/context.ts

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,55 @@
11
import * as React from "react";
22
import type {
3-
TrackedPromise,
3+
AgnosticRouteMatch,
4+
AgnosticIndexRouteObject,
5+
AgnosticNonIndexRouteObject,
46
History,
57
Location,
68
Router,
79
StaticHandlerContext,
810
To,
9-
AgnosticRouteObject,
10-
AgnosticRouteMatch,
11+
TrackedPromise,
1112
} from "@remix-run/router";
1213
import type { Action as NavigationType } from "@remix-run/router";
1314

1415
// Create react-specific types from the agnostic types in @remix-run/router to
1516
// export from react-router
16-
export interface RouteObject extends AgnosticRouteObject {
17+
export interface IndexRouteObject {
18+
caseSensitive?: AgnosticIndexRouteObject["caseSensitive"];
19+
path?: AgnosticIndexRouteObject["path"];
20+
id?: AgnosticIndexRouteObject["id"];
21+
loader?: AgnosticIndexRouteObject["loader"];
22+
action?: AgnosticIndexRouteObject["action"];
23+
hasErrorBoundary: AgnosticIndexRouteObject["hasErrorBoundary"];
24+
shouldRevalidate?: AgnosticIndexRouteObject["shouldRevalidate"];
25+
handle?: AgnosticIndexRouteObject["handle"];
26+
index: true;
27+
children?: undefined;
28+
element?: React.ReactNode | null;
29+
errorElement?: React.ReactNode | null;
30+
}
31+
32+
export interface NonIndexRouteObject {
33+
caseSensitive?: AgnosticNonIndexRouteObject["caseSensitive"];
34+
path?: AgnosticNonIndexRouteObject["path"];
35+
id?: AgnosticNonIndexRouteObject["id"];
36+
loader?: AgnosticNonIndexRouteObject["loader"];
37+
action?: AgnosticNonIndexRouteObject["action"];
38+
hasErrorBoundary: AgnosticNonIndexRouteObject["hasErrorBoundary"];
39+
shouldRevalidate?: AgnosticNonIndexRouteObject["shouldRevalidate"];
40+
handle?: AgnosticNonIndexRouteObject["handle"];
41+
index?: false;
1742
children?: RouteObject[];
1843
element?: React.ReactNode | null;
1944
errorElement?: React.ReactNode | null;
2045
}
2146

22-
export interface DataRouteObject extends RouteObject {
47+
export type RouteObject = IndexRouteObject | NonIndexRouteObject;
48+
49+
export type DataRouteObject = RouteObject & {
2350
children?: DataRouteObject[];
2451
id: string;
25-
}
52+
};
2653

2754
export interface RouteMatch<
2855
ParamKey extends string = string,

packages/router/__tests__/router-test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
} from "../index";
2929

3030
// Private API
31-
import type { TrackedPromise } from "../utils";
31+
import type { AgnosticRouteObject, TrackedPromise } from "../utils";
3232
import { AbortedDeferredError } from "../utils";
3333

3434
///////////////////////////////////////////////////////////////////////////////
@@ -1002,6 +1002,28 @@ describe("a router", () => {
10021002
);
10031003
});
10041004

1005+
it("throws if it finds index routes with children", async () => {
1006+
let routes = [
1007+
// @ts-expect-error
1008+
{
1009+
index: true,
1010+
children: [
1011+
{
1012+
path: "nope",
1013+
},
1014+
],
1015+
},
1016+
];
1017+
expect(() =>
1018+
createRouter({
1019+
routes,
1020+
history: createMemoryHistory(),
1021+
})
1022+
).toThrowErrorMatchingInlineSnapshot(
1023+
`"Cannot specify children on an index route"`
1024+
);
1025+
});
1026+
10051027
it("supports a basename prop for route matching", async () => {
10061028
let history = createMemoryHistory({
10071029
initialEntries: ["/base/name/path"],

packages/router/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@ import { convertRoutesToDataRoutes } from "./utils";
33
export type {
44
ActionFunction,
55
ActionFunctionArgs,
6+
AgnosticDataIndexRouteObject,
7+
AgnosticDataNonIndexRouteObject,
68
AgnosticDataRouteMatch,
79
AgnosticDataRouteObject,
10+
AgnosticIndexRouteObject,
11+
AgnosticNonIndexRouteObject,
812
AgnosticRouteMatch,
913
AgnosticRouteObject,
1014
TrackedPromise,

0 commit comments

Comments
 (0)