-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
211: Practice Exercise: Building a Type-Safe Router with Path Params
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, theproductIdproperty 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
createRoutethat takes two arguments:- A path string (e.g.,
"/blog/:postSlug"). - A handler function that accepts
paramsas its first argument.
- A path string (e.g.,
- Ensure that the
paramsargument 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
});
There are no comments for now.