-
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
112: Practice Exercise: Building a Type-Safe Router
I've seen this a dozen times in code reviews: a developer decides to make their routing "type-safe" by creating a huge union of strings. It looks like this: type Route = '/home' | '/profile' | '/settings' | '/user/:id'. On the surface, it feels like you've solved the problem. You get autocomplete, and you can't accidentally type '/setttings' with three 't's.
The 'Union of Strings' Trap
The problem arrives the second you have dynamic parameters. If your route is '/user/:id', that string is a pattern, not an actual destination. When you actually call your navigation function, you aren't navigating to the literal string '/user/:id'; you're navigating to '/user/123' or '/user/abc'.
type Route = '/home' | '/user/:id';
function navigate(path: Route) {
window.location.href = path;
}
// This works (but we probably didn't mean to go to the literal pattern)
navigate('/user/:id');
// This throws a TypeScript error, even though it's a perfectly valid URL
navigate('/user/123'); // Error: Argument of type '"/user/123"' is not assignable to parameter of type 'Route'.
This is a classic case of confusing a definition with an instance. Your union is defining the "shape" of the route, but TypeScript is treating it as the literal value. It's frustrating because the type system is technically doing exactly what you told it to do, but you told it the wrong thing.
Driving Type Safety from a Single Source of Truth
To fix this, we need to stop manually writing strings and start deriving our types from a configuration object. I prefer using a mapped type combined with TypeScript's template literal types. This allows us to define the parameters for each route once and have TypeScript calculate the resulting string patterns for us.
Instead of a union, let's define a RouteConfig. I like to use a record where the key is a friendly name for the route and the value defines the params it expects. If a route has no params, we can just use an empty object.
interface RouteConfig {
home: {};
userProfile: { userId: string };
productDetail: { productId: string; category: string };
}
// Here is where the magic happens.
// We create a helper that transforms our config into a type-safe function call.
type NavigateFn = {
[K in keyof RouteConfig]: (params: RouteConfig[K]) => string;
};
const router: NavigateFn = {
home: () => '/home',
userProfile: ({ userId }) => `/user/${userId}`,
productDetail: ({ productId, category }) => `/shop/${category}/${productId}`,
};
// Now, the compiler forces you to provide the correct params for the specific route
const url = router.userProfile({ userId: '42' }); // "/user/42"
// const error = router.userProfile({}); // Error: Property 'userId' is missing
By shifting the logic from "matching a string" to "calling a function based on a config," we've eliminated the possibility of a malformed URL. You aren't just checking if a string exists in a list; you're ensuring that the data required to build that string is present and correctly typed. It's a bit more boilerplate up front, but it saves you from those "undefined" or "NaN" segments in your URLs that usually only show up in production.
📋 Practical Task
Exercise: Implementing a Parametric Route Guard for an E-commerce Dashboard
You are building a dashboard for an e-commerce store. You need to implement a type-safe navigation system that prevents developers from navigating to product or order pages without providing the necessary IDs.
Requirements:
- Create a
DashboardRoutesinterface that defines three routes:overview(no parameters)orderDetail(requires anorderIdof typenumber)customerSupport(requires aticketIdof typestringand apriorityof'low' | 'high')
- Implement a
routerobject that maps these keys to functions returning the final URL string (e.g.,/orders/123). - Create a function
safeNavigate(routeName: keyof DashboardRoutes, params: any). Use a generic type constraint to ensure thatparamsmatches the requirements of therouteNameprovided. - Test your implementation by attempting to call
safeNavigatewith missing or incorrectly typed parameters to ensure the TypeScript compiler catches the errors.
There are no comments for now.