TypeScript Challenge #1: Route Path Interpolation
Posted on July 30, 2025
🧩 The Challenge
Create a utility type ExtractParams<Path>
that extracts dynamic segments from a route path string (e.g., '/tenant/[tenantSlug]/event/[id]'
) and produces a strongly typed object where the keys are the dynamic segment names (without the brackets) and the values are of type string
.
For example:
Do you know how resolve this challenge? Try to check it with the widget below
🚀 Practical application
🔹Example 1:
In many projects, it’s common to define route templates centrally, like so:
To construct a URL, you might implement a utility function like this:
This works… but it has a flaw: the params
object is loosely typed. TypeScript won't warn you if a required parameter is missing — you'll only find out at runtime via a thrown error Missing parameter:...
But we can improve it using this utility type ExtractParams
in this way:
With this, TypeScript ensures at compile-time that all required parameters are provided, significantly reducing runtime bugs like Missing parameter: ...
.
🔹Example 2 (React Native with Expo Router)
If you’re using Expo Router in a React Native project, you’re probably familiar with the useLocalSearchParams()
hook, which lets you access dynamic route parameters. However, the default return type is:
This is very loose and unsafe, and it doesn’t provide strong typing for the specific route you’re working with. But with the ExtractParams
utility type, we can type-safely infer the expected parameters for any given route.
🔹Example 3 (NextJS)
If you are using NextJS, this util function will also be useful for Client Components:
The same for Server Component:
💡 Solution
👣 Step 1: Understand Template Literal Types
In TypeScript, you can use template literal types to match parts of a string using pattern matching.
infer
lets you extract a piece of a type that matches a pattern.
For example, we may extract the first dynamic parameter (only one) from a path in this way:
Of course, it is not enough for us —we need to take a step further (see Step 2).
Before jumping to the next task, try this hands-on challenge to reinforce your grasp of template literal types and pattern matching.
🧩 Mini-Challenge 1:
Create a utility type SplitEmail<Email>
that takes a string representing an email address (e.g., "john.doe@example.com"
) and returns a tuple containing the username and domain as separate elements.
If the input is not a valid email format, the type should resolve to never
.
For simplicity, you do not need to handle invalid inputs with multiple @
symbols (e.g., user@domain@com
) — these will not appear in test cases. But you are still responsible for checking that the domain must contain at least one dot (.
) (e.g., example.com
is valid, but example
is not)
For example:
👣 Step 2: Understand Recursion Types
TypeScript supports a concept similar to JavaScript function recursion — known as recursive types.
A recursive type is a type that refers to itself in its definition in order to break a complex problem into smaller parts, just like a recursive function.
To successfully solve any recursion-based type challenge, we need to define two key components:
1. Base Case (Stopping Condition)
This is the condition that terminates the recursion. Without a base case, TypeScript would recurse infinitely and eventually hit a recursion depth error.
2. Recursive Step
This is the step where the type calls itself with a smaller or simpler version of the input — reducing the problem one layer at a time.
Let’s apply this concept to solve the following problem.
🧩 Mini-Challenge 2:
Create a utility type TrimLeft<Str>
that removes all leading whitespace (' '
, '\n'
, '\t'
, etc.) from the beginning of a string.
First, we create a union type that lists all characters we want to trim:
I always begin solving any recursion problem by defining the base case first. In our case, the base case is when the string does not start with a whitespace character — in that scenario, we simply return the string unchanged. Since we already learned how to match patterns using template literal types in the previous step, the base case can be expressed like this:
Now replace the never
placeholder with a recursive call to TrimLeft<Rest>
. This will continue stripping one whitespace character at a time:
Here is the trace of this recursion for example TrimLeft<'\n\t Hello'>
:

Try solving this challenge using the widget below to get some hands-on practice and deepen your understanding.
Now that you’ve seen how recursive types work, let’s take it one step closer to our final goal.
Previously, we only extracted the first dynamic segment from a route path provided as a string — now it’s time to recursively extract all dynamic parts.
🧩 Mini-Challenge 3:
Create a utility type ExtractRouteSegments<Path>
that should take a dynamic route path (like those used in Next.js or other routing systems) and return a union of all dynamic segment names (without brackets).
For example:
If you are struggling to resolve this challenge, the hint will be the following: keep matching the pattern ${string}[${infer Param}]${infer Rest}
, then recursively process Rest
.
🎉 We’re getting all dynamic param names — but as a union of strings, not an object.
👣 Step 3: Convert the Union to an Object Type
Now we want to convert 'tenantSlug' | 'id'
to:
We can do this with mapped types and key remapping:
🎯 Step 4 (Final Solution): Combining all together
By now, you should be equipped with everything you need to tackle this challenge. Let’s combine everything and complete the solution.