Hone logo
Hone
Problems

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 Route type that represents a route with a path (string) and a handler function.
  • Create a Router class that manages a collection of routes.
  • The Router class should have a method addRoute that adds a new route to the router.
  • The Router class should have a method handleRequest that 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 return undefined.
  • 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 Router class 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 handleRequest method should return undefined.
  • 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 handleRequest method 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.
Loading editor...
typescript