Skip to content

Commit b16b249

Browse files
committed
refactor: simplify types
1 parent 4493f6a commit b16b249

File tree

11 files changed

+111
-83
lines changed

11 files changed

+111
-83
lines changed

packages/router/src/Router.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import {
77
} from "react";
88
import { RouterContext } from "./context/RouterContext.js";
99
import { RouteContext } from "./context/RouteContext.js";
10-
import type {
11-
RouteDefinition,
12-
NavigateOptions,
13-
MatchedRouteWithData,
10+
import {
11+
type NavigateOptions,
12+
type MatchedRouteWithData,
13+
internalRoutes,
1414
} from "./types.js";
1515
import { matchRoutes } from "./core/matchRoutes.js";
1616
import {
@@ -22,13 +22,15 @@ import {
2222
getIdleAbortSignal,
2323
} from "./core/navigation.js";
2424
import { executeLoaders, createLoaderRequest } from "./core/loaderCache.js";
25+
import type { RouteDefinition } from "./route.js";
2526

2627
export type RouterProps = {
27-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
28-
routes: RouteDefinition<any>[];
28+
routes: RouteDefinition[];
2929
};
3030

31-
export function Router({ routes }: RouterProps): ReactNode {
31+
export function Router({ routes: inputRoutes }: RouterProps): ReactNode {
32+
const routes = internalRoutes(inputRoutes);
33+
3234
const currentEntry = useSyncExternalStore(
3335
subscribeToNavigation,
3436
getNavigationSnapshot,

packages/router/src/__tests__/Router.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Outlet } from "../Outlet.js";
55
import { useParams } from "../hooks/useParams.js";
66
import { useLocation } from "../hooks/useLocation.js";
77
import { setupNavigationMock, cleanupNavigationMock } from "./setup.js";
8-
import type { RouteDefinition } from "../types.js";
8+
import type { RouteDefinition } from "../route.js";
99

1010
describe("Router", () => {
1111
let mockNavigation: ReturnType<typeof setupNavigationMock>;

packages/router/src/__tests__/hooks.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { useLocation } from "../hooks/useLocation.js";
66
import { useParams } from "../hooks/useParams.js";
77
import { useSearchParams } from "../hooks/useSearchParams.js";
88
import { setupNavigationMock, cleanupNavigationMock } from "./setup.js";
9-
import type { RouteDefinition } from "../types.js";
9+
import type { RouteDefinition } from "../route.js";
1010

1111
describe("hooks", () => {
1212
let mockNavigation: ReturnType<typeof setupNavigationMock>;

packages/router/src/__tests__/loader.test.tsx

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
22
import { render, screen, act } from "@testing-library/react";
33
import { Router } from "../Router.js";
44
import { Outlet } from "../Outlet.js";
5-
import { route } from "../route.js";
5+
import { route, type LoaderArgs } from "../route.js";
66
import { setupNavigationMock, cleanupNavigationMock } from "./setup.js";
7-
import type { LoaderArgs, RouteDefinition } from "../types.js";
7+
import { internalRoutes, type InternalRouteDefinition } from "../types.js";
88
import { clearLoaderCache } from "../core/loaderCache.js";
99

1010
describe("Data Loader", () => {
@@ -387,22 +387,26 @@ describe("Data Loader", () => {
387387
type User = { id: number; name: string };
388388

389389
// This should compile without errors
390-
const userRoute = route({
391-
path: "/users/:id",
392-
loader: (): User => ({ id: 1, name: "Test" }),
393-
component: ({ data }: { data: User }) => <div>{data.name}</div>,
394-
}) as RouteDefinition<User>;
390+
const userRoute = internalRoutes([
391+
route({
392+
path: "/users/:id",
393+
loader: (): User => ({ id: 1, name: "Test" }),
394+
component: ({ data }: { data: User }) => <div>{data.name}</div>,
395+
}),
396+
])[0];
395397

396398
expect(userRoute.path).toBe("/users/:id");
397399
expect(userRoute.loader).toBeDefined();
398400
});
399401

400402
it("allows routes without loader and without data prop", () => {
401403
// This should compile without errors
402-
const aboutRoute = route({
403-
path: "/about",
404-
component: () => <div>About</div>,
405-
}) as RouteDefinition;
404+
const aboutRoute = internalRoutes([
405+
route({
406+
path: "/about",
407+
component: () => <div>About</div>,
408+
}),
409+
])[0];
406410

407411
expect(aboutRoute.path).toBe("/about");
408412
expect(aboutRoute.loader).toBeUndefined();

packages/router/src/__tests__/matchRoutes.test.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,32 @@
11
import { describe, it, expect } from "vitest";
22
import { matchRoutes } from "../core/matchRoutes.js";
3-
import type { RouteDefinition } from "../types.js";
3+
import { internalRoutes, type InternalRouteDefinition } from "../types.js";
44

55
describe("matchRoutes", () => {
66
describe("basic matching", () => {
77
it("matches exact paths", () => {
8-
const routes: RouteDefinition[] = [
8+
const routes = internalRoutes([
99
{ path: "/", component: () => null },
1010
{ path: "/about", component: () => null },
11-
];
11+
]);
1212

1313
const result = matchRoutes(routes, "/about");
1414
expect(result).toHaveLength(1);
1515
expect(result![0].route.path).toBe("/about");
1616
});
1717

1818
it("returns null for non-matching paths", () => {
19-
const routes: RouteDefinition[] = [
19+
const routes = internalRoutes([
2020
{ path: "/", component: () => null },
2121
{ path: "/about", component: () => null },
22-
];
22+
]);
2323

2424
const result = matchRoutes(routes, "/contact");
2525
expect(result).toBeNull();
2626
});
2727

2828
it("matches root path", () => {
29-
const routes: RouteDefinition[] = [{ path: "/", component: () => null }];
29+
const routes = internalRoutes([{ path: "/", component: () => null }]);
3030

3131
const result = matchRoutes(routes, "/");
3232
expect(result).toHaveLength(1);
@@ -36,19 +36,19 @@ describe("matchRoutes", () => {
3636

3737
describe("path parameters", () => {
3838
it("extracts single parameter", () => {
39-
const routes: RouteDefinition[] = [
39+
const routes = internalRoutes([
4040
{ path: "/users/:id", component: () => null },
41-
];
41+
]);
4242

4343
const result = matchRoutes(routes, "/users/123");
4444
expect(result).toHaveLength(1);
4545
expect(result![0].params).toEqual({ id: "123" });
4646
});
4747

4848
it("extracts multiple parameters", () => {
49-
const routes: RouteDefinition[] = [
49+
const routes = internalRoutes([
5050
{ path: "/users/:userId/posts/:postId", component: () => null },
51-
];
51+
]);
5252

5353
const result = matchRoutes(routes, "/users/42/posts/99");
5454
expect(result).toHaveLength(1);
@@ -58,7 +58,7 @@ describe("matchRoutes", () => {
5858

5959
describe("nested routes", () => {
6060
it("matches nested routes", () => {
61-
const routes: RouteDefinition[] = [
61+
const routes = internalRoutes([
6262
{
6363
path: "/",
6464
component: () => null,
@@ -67,7 +67,7 @@ describe("matchRoutes", () => {
6767
{ path: "about", component: () => null },
6868
],
6969
},
70-
];
70+
]);
7171

7272
const result = matchRoutes(routes, "/about");
7373
expect(result).toHaveLength(2);
@@ -76,7 +76,7 @@ describe("matchRoutes", () => {
7676
});
7777

7878
it("matches deeply nested routes", () => {
79-
const routes: RouteDefinition[] = [
79+
const routes = internalRoutes([
8080
{
8181
path: "/",
8282
component: () => null,
@@ -88,7 +88,7 @@ describe("matchRoutes", () => {
8888
},
8989
],
9090
},
91-
];
91+
]);
9292

9393
const result = matchRoutes(routes, "/users/123");
9494
expect(result).toHaveLength(3);
@@ -99,27 +99,27 @@ describe("matchRoutes", () => {
9999
});
100100

101101
it("merges params from parent routes", () => {
102-
const routes: RouteDefinition[] = [
102+
const routes = internalRoutes([
103103
{
104104
path: "/org/:orgId",
105105
component: () => null,
106106
children: [{ path: "users/:userId", component: () => null }],
107107
},
108-
];
108+
]);
109109

110110
const result = matchRoutes(routes, "/org/acme/users/123");
111111
expect(result).toHaveLength(2);
112112
expect(result![1].params).toEqual({ orgId: "acme", userId: "123" });
113113
});
114114

115115
it("matches index route (empty path)", () => {
116-
const routes: RouteDefinition[] = [
116+
const routes = internalRoutes([
117117
{
118118
path: "/",
119119
component: () => null,
120120
children: [{ path: "", component: () => null }],
121121
},
122-
];
122+
]);
123123

124124
const result = matchRoutes(routes, "/");
125125
expect(result).toHaveLength(2);
@@ -130,10 +130,10 @@ describe("matchRoutes", () => {
130130

131131
describe("route priority", () => {
132132
it("matches first matching route", () => {
133-
const routes: RouteDefinition[] = [
133+
const routes = internalRoutes([
134134
{ path: "/users/new", component: () => null },
135135
{ path: "/users/:id", component: () => null },
136-
];
136+
]);
137137

138138
const result = matchRoutes(routes, "/users/new");
139139
expect(result).toHaveLength(1);

packages/router/src/core/loaderCache.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
import type { LoaderArgs } from "../route.js";
12
import type {
2-
LoaderArgs,
33
MatchedRoute,
44
MatchedRouteWithData,
5-
RouteDefinition,
5+
InternalRouteDefinition,
66
} from "../types.js";
77

88
/**
@@ -17,7 +17,7 @@ const loaderCache = new Map<string, unknown>();
1717
*/
1818
function getOrCreateLoaderResult(
1919
entryId: string,
20-
route: RouteDefinition<unknown>,
20+
route: InternalRouteDefinition,
2121
args: LoaderArgs,
2222
): unknown | undefined {
2323
if (!route.loader) {

packages/router/src/core/matchRoutes.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import type { RouteDefinition, MatchedRoute } from "../types.js";
1+
import type { InternalRouteDefinition, MatchedRoute } from "../types.js";
22

33
/**
44
* Match a pathname against a route tree, returning the matched route stack.
55
* Returns null if no match is found.
66
*/
77
export function matchRoutes(
8-
routes: RouteDefinition[],
8+
routes: InternalRouteDefinition[],
99
pathname: string,
1010
): MatchedRoute[] | null {
1111
for (const route of routes) {
@@ -21,7 +21,7 @@ export function matchRoutes(
2121
* Match a single route and its children recursively.
2222
*/
2323
function matchRoute(
24-
route: RouteDefinition,
24+
route: InternalRouteDefinition,
2525
pathname: string,
2626
): MatchedRoute[] | null {
2727
const hasChildren = Boolean(route.children?.length);

packages/router/src/core/navigation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { RouteDefinition, NavigateOptions } from "../types.js";
1+
import type { InternalRouteDefinition, NavigateOptions } from "../types.js";
22
import { matchRoutes } from "./matchRoutes.js";
33
import { executeLoaders, createLoaderRequest } from "./loaderCache.js";
44

@@ -71,7 +71,7 @@ export function getServerSnapshot(): null {
7171
* Returns a cleanup function.
7272
*/
7373
export function setupNavigationInterception(
74-
routes: RouteDefinition[],
74+
routes: InternalRouteDefinition[],
7575
): () => void {
7676
if (!hasNavigation()) {
7777
return () => {};

packages/router/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ export { route } from "./route.js";
1515

1616
// Types
1717
export type {
18-
RouteDefinition,
1918
MatchedRoute,
2019
MatchedRouteWithData,
2120
NavigateOptions,
2221
Location,
23-
LoaderArgs,
2422
} from "./types.js";
23+
24+
export type { LoaderArgs, RouteDefinition } from "./route.js";

packages/router/src/route.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,39 @@
11
import type { ComponentType } from "react";
2-
import type { LoaderArgs, RouteDefinition } from "./types.js";
32

4-
const routeDefinitionSymbol = Symbol("RouteDefinition");
3+
const routeDefinitionSymbol = Symbol();
4+
5+
/**
6+
* Arguments passed to loader functions.
7+
*/
8+
export type LoaderArgs = {
9+
/** Extracted path parameters */
10+
params: Record<string, string>;
11+
/** Request object with URL and headers */
12+
request: Request;
13+
/** AbortSignal for cancellation on navigation */
14+
signal: AbortSignal;
15+
};
516

617
/**
718
* Route definition created by the `route` helper function.
819
*/
920
export interface OpaqueRouteDefinition {
1021
[routeDefinitionSymbol]: never;
1122
path: string;
12-
children?: OpaqueRouteDefinition[];
23+
children?: RouteDefinition[];
1324
}
1425

26+
/**
27+
* Any route definition defined by user.
28+
*/
29+
export type RouteDefinition =
30+
| OpaqueRouteDefinition
31+
| {
32+
path: string;
33+
component?: ComponentType<{}>;
34+
children?: RouteDefinition[];
35+
};
36+
1537
/**
1638
* Route definition with loader - infers TData from loader return type.
1739
*/
@@ -20,7 +42,7 @@ type RouteWithLoader<TData> = {
2042
loader: (args: LoaderArgs) => TData;
2143
component: ComponentType<{ data: TData }>;
2244
// eslint-disable-next-line @typescript-eslint/no-explicit-any
23-
children?: RouteDefinition<any>[];
45+
children?: RouteDefinition[];
2446
};
2547

2648
/**
@@ -30,7 +52,7 @@ type RouteWithoutLoader = {
3052
path: string;
3153
component?: ComponentType;
3254
// eslint-disable-next-line @typescript-eslint/no-explicit-any
33-
children?: RouteDefinition<any>[];
55+
children?: RouteDefinition[];
3456
};
3557

3658
/**

0 commit comments

Comments
 (0)