Challenge 2: Add Route Groups — Possible Solution ==================================================================== type ExtractParams = Path extends `${string}:${infer Param}/${infer Rest}` ? { [K in Param | keyof ExtractParams]: string } : Path extends `${string}:${infer Param}` ? { [K in Param]: string } : {}; type Handler = (req: { params: ExtractParams }) => void; type Routes = Record>; // Key remapping (the `as` clause in a mapped type) rebuilds each route's // key with the prefix prepended, using a template literal type. type PrefixRoutes = { [K in keyof R as K extends string ? `${Prefix}${K}` : never]: R[K]; }; class Router { // Exposed (not private) so .group() below can read another router // instance's accumulated routes directly. readonly routes: R; constructor(routes: R) { this.routes = routes; } get( path: Path, handler: Handler ): Router>> { return new Router({ ...this.routes, [path]: handler } as R & Record>); } group( prefix: Prefix, callback: (router: Router<{}>) => Router ): Router> { const groupRouter = callback(new Router({})); const prefixedRoutes = {} as Record>; for (const key of Object.keys(groupRouter.routes)) { prefixedRoutes[`${prefix}${key}`] = groupRouter.routes[key]; } return new Router({ ...this.routes, ...prefixedRoutes } as R & PrefixRoutes); } } // --- Usage --- const router = new Router({}) .get("/health", (req) => console.log("ok")) .group("/api", (r) => r .get("/users/:id", (req) => console.log(`User ${req.params.id}`)) .get("/posts/:postId", (req) => console.log(`Post ${req.params.postId}`)) ); // The resulting router's registered routes are typed as: // "/health" | "/api/users/:id" | "/api/posts/:postId" // even though the group's callback only ever registered "/users/:id" and // "/posts/:postId" — PrefixRoutes computed the "/api" + path combination // entirely at the type level. WHY THIS WORKS -------------- - PrefixRoutes uses mapped type key remapping (`[K in keyof R as ...]`) combined with a template literal (`${Prefix}${K}`) to build a NEW set of keys, each with the prefix prepended — this is the same template-literal-over-a-union distribution from Chapter 6/8, just applied to object keys instead of a plain string union. - group()'s callback receives a fresh, empty Router<{}> so route paths registered INSIDE the group are written without the prefix (just "/users/:id", not "/api/users/:id") — the prefixing happens once, automatically, when the group's routes are merged back into the outer router, rather than requiring every route inside the group to repeat the prefix by hand. - The runtime loop (`for (const key of Object.keys(groupRouter.routes))`) does the equivalent prefixing at the value level, keeping the runtime behavior consistent with what PrefixRoutes computed at the type level — exactly the "type declaration and implementation must agree" principle from the Module Augmentation chapter's polyfill discussion.