Skip to content

Commit 5c21adb

Browse files
committed
feat: router with as const type parameters
1 parent c8867f3 commit 5c21adb

File tree

3 files changed

+117
-0
lines changed

3 files changed

+117
-0
lines changed

src/const-type-parameter/basic-example.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable @typescript-eslint/no-unused-vars */
12
// Example with "as const"
23
export type User = { name: string; age: number };
34

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/* eslint-disable @typescript-eslint/no-unused-vars */
2+
type ExtractRouteParams<T> = T extends `:${infer P}/${infer Rest}`
3+
? P | ExtractRouteParams<`${Rest}`>
4+
: T extends `:${infer P}`
5+
? P
6+
: T extends `${infer _Start}/${infer Rest}`
7+
? ExtractRouteParams<Rest>
8+
: never;
9+
10+
type RouteParamsObject<T extends string> = {
11+
[key in T]: string;
12+
};
13+
14+
export type Route = {
15+
name: string;
16+
path: string;
17+
};
18+
19+
function buildRouter<const T extends Route>(routes: T[]) {
20+
const getRoute = <
21+
RouteName extends T["name"],
22+
RouteParams extends RouteParamsObject<
23+
ExtractRouteParams<Extract<T, { name: RouteName }>["path"]>
24+
>
25+
>(
26+
name: RouteName,
27+
params: RouteParams
28+
): string => {
29+
const route = routes.find((n) => n.name && n.name === name)!.path;
30+
31+
Object.entries<string>(params).forEach(([key, value]) => {
32+
route.replace(`:${key}`, value);
33+
});
34+
35+
return route;
36+
};
37+
return { getRoute };
38+
}
39+
40+
// Ejemplo de narrowing que explicando el porque funciona la firma anterior
41+
type GeneratedRoutesType =
42+
| { name: "login"; path: "/login"; params: [] }
43+
| { name: "courses"; path: "/courses/:category/:year"; params: ["category"] };
44+
45+
type RouteNames = GeneratedRoutesType["name"];
46+
// ^?
47+
48+
type RouteParams = Extract<GeneratedRoutesType, { name: "courses" }>["params"];
49+
// ^?
50+
51+
type RoutePath = Extract<GeneratedRoutesType, { name: "courses" }>["path"];
52+
// ^?
53+
54+
const { getRoute } = buildRouter([
55+
{
56+
name: "login",
57+
path: "/login",
58+
},
59+
{
60+
name: "courses",
61+
path: "/courses/:category",
62+
},
63+
]);
64+
65+
const route = getRoute("courses", { category: "TypeScript" });
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/* eslint-disable @typescript-eslint/no-unused-vars */
2+
export type Route = {
3+
name: string;
4+
path: string;
5+
params: readonly string[];
6+
};
7+
8+
function buildRouter<const T extends Route>(routes: T[]) {
9+
const getRoute = <
10+
RouteName extends T["name"],
11+
RouteParams extends Extract<T, { name: RouteName }>["params"]
12+
>(
13+
name: RouteName,
14+
params: { [key in RouteParams[number]]: string }
15+
): string => {
16+
const route = routes.find((n) => n.name && n.name === name)!.path;
17+
18+
Object.entries<string>(params).forEach(([key, value]) => {
19+
route.replace(`:${key}`, value);
20+
});
21+
22+
return route;
23+
};
24+
return { getRoute };
25+
}
26+
27+
// Ejemplo de narrowing que explicando el porque funciona la firma anterior
28+
type GeneratedRoutesType =
29+
| { name: "login"; path: "/login"; params: [] }
30+
| { name: "courses"; path: "/courses"; params: ["category"] };
31+
32+
type RouteNames = GeneratedRoutesType["name"];
33+
// ^?
34+
35+
type RouteParams = Extract<GeneratedRoutesType, { name: "courses" }>["params"];
36+
// ^?
37+
38+
const { getRoute } = buildRouter([
39+
{
40+
name: "login",
41+
path: "/login",
42+
params: [],
43+
},
44+
{
45+
name: "courses",
46+
path: "/courses/:category",
47+
params: ["category"],
48+
},
49+
]);
50+
51+
const route = getRoute("courses", { category: "typescript" });

0 commit comments

Comments
 (0)