Logo

TypeScript Challenge #1: Route Path Interpolation

CN
vladtopolev

Posted on July 30, 2025

TypeScript
TypeScript Challenge

🧩 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:

Loading...


Do you know how resolve this challenge? Try to check it with the widget below


Route Path Interpolation
Test Cases:0 of 3 passed
NOT RUN

Task:

ExtractParams<'/tenant'>

Expected result:

{}

NOT RUN

Task:

ExtractParams<'/tenant/[tenantSlug]'>

Expected result:

{ tenantSlug: string }

NOT RUN

Task:

ExtractParams<'/tenant/[tenantSlug]/event/[id]'>

Expected result:

{ tenantSlug: string, id: string }

Your Code
Loading...



🚀 Practical application

🔹Example 1:

In many projects, it’s common to define route templates centrally, like so:

Loading...

To construct a URL, you might implement a utility function like this:

Loading...

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:

Loading...

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:

Loading...

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.

Loading...


🔹Example 3 (NextJS)

If you are using NextJS, this util function will also be useful for Client Components:

Loading...

The same for Server Component:

Loading...

💡 Solution

👣 Step 1: Understand Template Literal Types

In TypeScript, you can use template literal types to match parts of a string using pattern matching.

Loading...


 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:

Loading...

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:

Loading...


String: SplitEmail
Test Cases:0 of 5 passed
NOT RUN

Task:

SplitEmail<'john.doe@example.com'>

Expected result:

['john.doe', 'example.com']

NOT RUN

Task:

SplitEmail<'admin+001@typescript.org'>

Expected result:

['admin+001', 'typescript.org']

NOT RUN

Task:

SplitEmail<'admin@subdomain.domain.com'>

Expected result:

['admin', 'subdomain.domain.com']

NOT RUN

Task:

SplitEmail<'invalid.email'>

Expected result:

never

NOT RUN

Task:

SplitEmail<'invalid.email@test'>

Expected result:

never

Your Code
Loading...


👣 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:

Loading...

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:

Loading...

Now replace the never placeholder with a recursive call to TrimLeft<Rest>. This will continue stripping one whitespace character at a time:

Loading...

Here is the trace of this recursion for example TrimLeft<'\n\t Hello'>:

Rrcursion Trace


Try solving this challenge using the widget below to get some hands-on practice and deepen your understanding.


String: TrimLeft
Test Cases:0 of 7 passed
NOT RUN

Task:

TrimLeft<'Hello World '>

Expected result:

'Hello World '

NOT RUN

Task:

TrimLeft<' Hello World '>

Expected result:

'Hello World '

NOT RUN

Task:

TrimLeft<' Hello World '>

Expected result:

'Hello World '

NOT RUN

Task:

TrimLeft<''>

Expected result:

''

NOT RUN

Task:

<TrimLeft<' '>

Expected result:

''

NOT RUN

Task:

TrimLeft<' \n\n\t Hello World'>

Expected result:

'Hello World'

NOT RUN

Task:

TrimLeft<' \n\n\t '>

Expected result:

''

Your Code
Loading...

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:

Loading...


String: Route Path Interpolation As Union Type
Test Cases:0 of 4 passed
NOT RUN

Task:

ExtractRouteSegments<'/about'>

Expected result:

never

NOT RUN

Task:

ExtractRouteSegments<'/tenant/[tenantSlug]'>

Expected result:

'tenantSlug'

NOT RUN

Task:

ExtractRouteSegments<'/user/[userId]/settings/[tab]'>

Expected result:

'userId' | 'tab'

NOT RUN

Task:

ExtractRouteSegments<'/[lang]/[region]/[page]'>

Expected result:

'lang' | 'region' | 'page'

Your Code
Loading...

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.

Loading...

🎉 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:

Loading...

We can do this with mapped types and key remapping:

Loading...


🎯 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.

Loading...