Skip to content

feat: type-safe router labels via per-label userData map#3747

Merged
B4nan merged 7 commits into
masterfrom
feat/typed-router-label-map
Jun 22, 2026
Merged

feat: type-safe router labels via per-label userData map#3747
B4nan merged 7 commits into
masterfrom
feat/typed-router-label-map

Conversation

@B4nan

@B4nan B4nan commented Jun 16, 2026

Copy link
Copy Markdown
Member

What

Lets users declare a label → userData map once and pass it as the router's Routes type argument, so route handlers get request.userData typed by label — and unknown labels become a compile-time error.

interface Routes {
    PRODUCT: { sku: string; price: number };
    CATEGORY: { categoryId: string };
}

const router = createCheerioRouter<CheerioCrawlingContext, Routes>();

router.addHandler('PRODUCT', async ({ request }) => {
    request.userData.sku;   // string
    request.userData.price; // number
});

router.addHandler('TYPO', async () => {}); // ❌ compile error: not a known label

How

  • New Routes generic on Router, RouterHandler and RouterRoutes, defaulting to an open Record<string, …> so existing untyped usage is unchanged.
  • Router.create and all 10 createXRouter factories expose two overloads — a route-map form (per-label typing) and the legacy flat-userData form. The second type argument selects between them: a route map matches the first overload, any other shape falls through to the second. This makes the documented createXRouter<Ctx, Routes>() form work without changing the meaning of the released <Ctx, UserData> type argument, keeping it fully backwards compatible. (The sole ambiguous case — a flat userData whose every field is itself an object — is read as a route map; a single scalar field disambiguates it.)
  • addHandler gains two overloads: a label-strict one (infers userData from the label) tried first, and the existing <UserData>-generic one as the fallback. The default handler receives the union of all declared userData shapes.
  • The route-map constraint uses the F-bounded Record<keyof Routes, Dictionary> form so both interface and type route maps are accepted (a plain Record<string, …> constraint silently rejects interface declarations).

This is a compile-time-only change (zero runtime cost). A follow-up targeting v4 adds an opt-in Standard Schema variant that also validates userData at runtime.

Relates to #3082

🤖 Generated with Claude Code

@B4nan B4nan added the adhoc Ad-hoc unplanned task added during the sprint. label Jun 16, 2026
@B4nan B4nan closed this Jun 16, 2026
@B4nan B4nan force-pushed the feat/typed-router-label-map branch from 25c063b to 4db2235 Compare June 16, 2026 12:32
Allow declaring a `label -> userData` map as the router's `Routes` type
argument (e.g. `createCheerioRouter<Context, Routes>()`). Route handlers
registered with `addHandler` then get `request.userData` typed by label,
and unknown labels are rejected at compile time when a map is declared.

The change is fully backwards compatible: the default `Routes` is an open
`Record<string, ...>`, so existing untyped usage and the per-handler
`addHandler<UserData>` generic keep working unchanged.

Relates to #3082
@B4nan B4nan reopened this Jun 16, 2026
@github-actions github-actions Bot added this to the 143rd sprint - Tooling team milestone Jun 16, 2026
@github-actions github-actions Bot added t-tooling Issues with this label are in the ownership of the tooling team. tested Temporary label used only programatically for some analytics. labels Jun 16, 2026
B4nan added 3 commits June 17, 2026 16:45
…oads

The documented `createXRouter<Ctx, Routes>()` two-argument form bound
`Routes` to the released `UserData` type parameter, so it silently fell
back to an open map instead of giving per-label `userData` typing. Making
`Routes` the second parameter would change the meaning of a released
positional type argument.

Instead, `Router.create` and the `createXRouter` factories now expose two
overloads — a route-map form (F-bounded, per-label typing) and the legacy
flat-`userData` form. The second type argument selects between them, so the
documented form works while existing `<Ctx, UserData>` callers fall through
to the original behavior. Runtime behavior is unchanged.
Add a "Type-safe router labels and `userData`" section to the TypeScript
projects guide explaining how to declare a route map and pass it as the
second type argument to a `createXRouter` factory for per-label `userData`
typing. Mirrored into the latest (3.17) docs snapshot for the patch release.
Regenerate the latest-stable (3.17) `api-typedoc.json` from current source
via `docusaurus api:version 3.17`, so the API reference reflects the typed
router-label map changes (per-label `RouterLabel`/`RouterHandlerContext`,
the `createXRouter`/`Router.create` overloads, and the removed `RouteMap`)
for the patch release.
Comment thread docs/guides/typescript_project.mdx Outdated
B4nan added 2 commits June 18, 2026 18:35
`addDefaultHandler` is a fallback that can receive any request (including
labels not in the route map), so defaulting its `userData` to the union of
declared route-map shapes over-promised and forced casting. Default it to
the context's `userData` type instead (matching `addHandler`'s fallback
overload and the pre-existing behavior) — which is `Record<string, unknown>`
for a typed-route-map router.
@B4nan B4nan requested a review from janbuchar June 22, 2026 10:28

@janbuchar janbuchar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, please address the one docs-related comment.

Comment thread docs/guides/typescript_project.mdx Outdated
Address review feedback: reword the intro so it describes that different
labels usually carry different `request.userData` shapes (with examples)
instead of claiming every handler reads `userData`.

Co-authored-by: Jan Buchar (review feedback)
@B4nan B4nan merged commit 1124aca into master Jun 22, 2026
9 checks passed
@B4nan B4nan deleted the feat/typed-router-label-map branch June 22, 2026 13:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

adhoc Ad-hoc unplanned task added during the sprint. t-tooling Issues with this label are in the ownership of the tooling team. tested Temporary label used only programatically for some analytics.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants