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

    ↑↓ move · ⏎ open · esc close

    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. Tab indents; press Escape then Tab to move focus out.

    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

    Fewer than 5 people have solved this one so far. The distribution appears once there is enough of a sample to mean anything.

    Short game

    Fewest characters

    No public scores yet.

    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.

    Uses