Skip to content
webtype.org#214 route-params · par 3

No. 214 · July 22, 2026 · Moderate

Route Params

Implement `ExtractParams<Path>` so every `:param` segment of a route string becomes a key of an object type, each valued `string`. The result must be a single flat object — an intersection will not pass.

01

Try the puzzle yourself

Par 3

Puzzle

route-params.ts
type Prettify<T> = { [K in keyof T]: T[K] } & {}
Stroke 1 of 3Not run yet

Replace ??? — your solution is checked against the cases below.

Checks

4
  • ExtractParams<'/about'>
    {}
  • ExtractParams<'/users/:id'>
    { id: string }
  • ExtractParams<'/users/:id/posts/:postId'>
    { id: string; postId: string }
  • ExtractParams<'/:org/:repo/issues/:number'>
    { org: string; repo: string; number: string }

How a check is judged Exact type equality, not assignability — an intersection is not the same as the flattened object.

How everyone did

Nobody is counting yet. Score distribution and the short game board arrive with accounts — until then your results stay on this device.

Archive
02

Annotated solution

Published July 23, 2026

The solution

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

The common wrong answer

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

Everything here is right except the shape of the answer. This produces `{ id: string } & { postId: string }`, which is assignable to `{ id: string; postId: string }` but is not the same type — and the checks test exact equality. Wrapping the whole thing in `Prettify` collapses the intersection into one object.

Line by line

  1. `${string}:${infer Param}/${infer Rest}`

    The leading `${string}` absorbs whatever comes before the colon, so the pattern does not care that the path starts with `/users`. `Param` binds up to the next slash; `Rest` takes everything after it.

  2. ExtractParams<`/${Rest}`>

    The slash is put back before recursing. Without it, a trailing segment like `posts/:postId` would still match the first pattern and the recursion would not terminate cleanly.

  3. Path extends `${string}:${infer Param}`

    The second pattern has no trailing slash, so it only matches when the parameter is the last thing in the string. This is the base case that stops the recursion.

  4. Prettify<...>

    Maps over the accumulated keys once more, producing a fresh object type. The `& {}` at the end of `Prettify` is what forces TypeScript to actually evaluate the mapped type rather than keeping it lazy.

Takeaway

Template literal patterns match greedily from the left. When you accumulate object types through recursion you get an intersection, and exact-equality checks will reject it — flatten before you return.