Type-Safe Routing System in TypeScript
Building a robust and maintainable web application often involves complex routing logic. This challenge asks you to design and implement a type-safe routing system in TypeScript, ensuring that route paths and associated handler functions are strongly typed, reducing runtime errors and improving developer experience. A type-safe routing system helps prevent common mistakes like typos in route paths or passing incorrect arguments to handler functions.
Problem Description
You are tasked with creating a routing system that allows you to define routes with specific paths and associated handler functions. The system should enforce type safety, ensuring that the route paths match the expected parameters in the handler functions. The system should be extensible, allowing for easy addition of new routes.
What needs to be achieved:
- Define a
Routetype that represents a route with a path (string) and a handler function. - Create a
Routerclass that manages a collection of routes. - The
Routerclass should have a methodaddRoutethat adds a new route to the router. - The
Routerclass should have a methodhandleRequestthat takes a request path (string) and returns the result of the handler function associated with the matching route. If no route matches the request path, it should returnundefined. - The routing system should be type-safe, meaning that the compiler should be able to verify that the route paths and handler function parameters are compatible.
Key Requirements:
- Type Safety: Route paths and handler function parameters must be strongly typed. The system should prevent routes with mismatched paths and handler parameters at compile time.
- Parameter Extraction: The routing system should be able to extract parameters from the route path and pass them to the handler function. Assume route parameters are denoted by colons (
:) in the path (e.g.,/users/:id). - Extensibility: The system should be easily extensible to support new routes and parameter types.
- Clear API: The
Routerclass should have a clear and concise API.
Expected Behavior:
- When a request path matches a route, the handler function associated with that route should be executed with the extracted parameters.
- If no route matches the request path, the
handleRequestmethod should returnundefined. - The compiler should flag any type errors related to route paths and handler function parameters.
Edge Cases to Consider:
- Routes with no parameters.
- Routes with multiple parameters.
- Request paths that do not match any defined routes.
- Handler functions that do not accept any parameters.
- Handler functions that accept parameters of different types.
- Route paths with optional parameters (e.g.,
/users/:id?). (This is not required for the initial solution, but consider it for future expansion).
Examples
Example 1:
// Route definition
const getUserRoute = (path: `/users/:id`, handler: (id: string) => string) => ({ path, handler });
// Router instantiation
const router = new Router<string>();
// Adding the route
router.addRoute(getUserRoute("/users/:id", (id) => `User with ID: ${id}`));
// Handling a request
const result = router.handleRequest("/users/123");
console.log(result); // Output: User with ID: 123
const result2 = router.handleRequest("/users/abc");
console.log(result2); // Output: User with ID: abc
Explanation: The getUserRoute function creates a route with the path /users/:id and a handler function that takes a string id as input. The router adds this route and then handles the request /users/123, correctly extracting the id parameter and passing it to the handler.
Example 2:
// Route definition
const getProductRoute = (path: `/products/:productId`, handler: (productId: number) => number) => ({ path, handler });
// Router instantiation
const router = new Router<number>();
// Adding the route
router.addRoute(getProductRoute("/products/:productId", (productId) => productId * 2));
// Handling a request
const result = router.handleRequest("/products/5");
console.log(result); // Output: 10
const result2 = router.handleRequest("/products/abc"); // This will cause a compile-time error because the handler expects a number.
Explanation: This example demonstrates type safety. The getProductRoute function defines a route that expects a number as a parameter. If you try to handle a request with a non-numeric parameter, the TypeScript compiler will flag an error.
Example 3: (No matching route)
const router = new Router<string>();
router.addRoute(getUserRoute("/users/:id", (id) => `User with ID: ${id}`));
const result = router.handleRequest("/products/123");
console.log(result); // Output: undefined
Explanation: Since there's no route defined for /products/123, the handleRequest method returns undefined.
Constraints
- Route Path Format: Route paths must follow the format
/path/:parameterName. Parameter names can only contain alphanumeric characters and underscores. - Parameter Types: Parameter types must be primitive types (string, number, boolean) or
any. - Performance: The
handleRequestmethod should have a time complexity of O(n) in the worst case, where n is the number of routes. - No external libraries: You are not allowed to use external routing libraries.
Notes
- Consider using generics to make the router type-safe for different parameter types.
- Think about how to handle route matching efficiently. Regular expressions can be helpful, but be mindful of their performance implications.
- Focus on creating a clear and well-documented API.
- Start with a simple implementation and gradually add more features as needed.
- The goal is to demonstrate type safety and a clean design, not to create a production-ready routing library.