Skip to Content
Course content

211: Practice Exercise: Building a Type-Safe Router with Path Params

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Look, we've all been there. You're building a router, and you want the parameters—like the id in /user/:id—to be type-safe. The first instinct for most developers is to reach for a Record<string, string>. It feels right because, technically, path parameters *are* just a collection of strings keyed by strings.

Thinking a generic Record is "type-safe enough"

The problem is that Record<string, string> isn't actually providing safety; it's providing permission. It tells TypeScript, "I can ask for any key I want, and I'll get a string back." But in a real router, that's a lie. If your route is /products/:productId, asking for params.category should be a compile-time error, not a runtime undefined.

// The "Fake" Safety Approach
type Params = Record<string, string>;

function handleProductPage(params: Params) {
  // TypeScript is happy, but this is a bug. 
  // The route is /products/:productId, so 'category' doesn't exist.
  console.log(params.category.toUpperCase()); 
}


I've seen this lead to some nasty production bugs where a developer renamed a path parameter in the route definition but forgot to update the handler. The code compiled perfectly, but the app crashed the moment a user hit that page.

Mapping Path Strings to Parameter Types

To fix this, we need TypeScript to actually "read" our route strings. We can achieve this using Template Literal Types. Instead of telling TS what the params are, we make TS derive the params from the path itself.

We can create a utility type that looks for the : character and extracts the following word. It's a bit of "type gymnastics," but it's incredibly powerful. Once we have a way to extract those keys, we can map them into a type where only those specific keys are allowed.

type ExtractParams<T extends string> = 
  T extends `${infer Start}:${infer Param}/${infer Rest}` 
    ? { [K in Param]: string } & ExtractParams<Rest> 
    : T extends `${infer Start}:${infer Param}` 
      ? { [K in Param]: string } 
      : {};

// Now, let's see it in action
type ProductRoute = "/products/:productId/:variant";
type ProductParams = ExtractParams<ProductRoute>;
// Result: { productId: string } & { variant: string }

const params: ProductParams = {
  productId: "123",
  variant: "blue",
  category: "electronics" // Error! 'category' does not exist in ProductParams
};


Now the compiler is actually acting as a guard. If you change the route string to /products/:id, the productId property in your handler will immediately turn red. That's the kind of tight feedback loop we want. It transforms the route string from a simple piece of data into the "single source of truth" for your type system.




📋 Practical Task

Implementing the RouteParams Utility and Type-Safe Route Handler

Your task is to build a mini-routing system that prevents developers from accessing non-existent path parameters. You need to implement a type-safe route function that links a path string to a handler function.

Requirements:

  • Create a type RouteParams<T> that extracts all parameters starting with : from a string path (supporting multiple parameters).
  • Implement a function createRoute that takes two arguments:
    1. A path string (e.g., "/blog/:postSlug").
    2. A handler function that accepts params as its first argument.
  • Ensure that the params argument in the handler is automatically typed based on the path string provided.

Starter Code:

type RouteParams<T extends string> = // Your implementation here

function createRoute<T extends string>(path: T, handler: (params: RouteParams<T>) => void) {
  // implementation logic doesn't matter for this exercise, 
  // just the type signatures
}

// Test Case: This should compile
createRoute("/user/:userId/posts/:postId", (params) => {
  console.log(params.userId); 
  console.log(params.postId);
});

// Test Case: This should throw a TypeScript error
createRoute("/user/:userId", (params) => {
  console.log(params.postId); // Error: Property 'postId' does not exist
});
Rating
0 0

There are no comments for now.

to be the first to leave a comment.