@@ -9,18 +9,21 @@ import type { Awaitable } from './typedefs.js';
99const defaultRoute = Symbol ( 'default-route' ) ;
1010
1111/**
12- * A map of request labels to the shape of `request.userData` expected for that label. Pass it as the
13- * `Routes` type argument of {@apilink Router} (or a `createXRouter` factory) to get per-label typing of
14- * `request.userData` and autocomplete/validation of labels in {@apilink Router.addHandler}.
15- *
16- * ```ts
17- * interface MyRoutes {
18- * PRODUCT: { sku: string; price: number };
19- * CATEGORY: { categoryId: string };
20- * }
21- * ```
12+ * The crawling context received by a route handler, with `request.userData` narrowed to `UserData`.
13+ */
14+ export type RouterHandlerContext < Context , UserData extends Dictionary > = Omit < Context , 'request' > & {
15+ request : LoadedRequest < Request < UserData > > ;
16+ } ;
17+
18+ /**
19+ * The set of labels accepted by {@apilink Router.addHandler}. When the router declares a concrete
20+ * route map (e.g. `{ PRODUCT: ...; CATEGORY: ... }`), only those labels (plus symbols) are
21+ * allowed — unknown labels become a compile-time error. When the map is left open (the default
22+ * `Record<string, ...>`), any string or symbol label is accepted, preserving the original behaviour.
2223 */
23- export type RouteMap = Record < string , Dictionary > ;
24+ export type RouterLabel < Routes extends Record < keyof Routes , Dictionary > > = string extends keyof Routes
25+ ? string | symbol
26+ : ( keyof Routes & string ) | symbol ;
2427
2528/**
2629 * A map of request labels to a [Standard Schema](https://standardschema.dev) (Zod, Valibot, ArkType, …)
@@ -31,32 +34,15 @@ export type RouteMap = Record<string, Dictionary>;
3134export type RouteSchemas = Record < string , StandardSchemaV1 > ;
3235
3336/**
34- * Derives a { @apilink RouteMap} (label → `userData` type) from a {@apilink RouteSchemas} map by inferring
35- * each schema's output type. Outputs that are not object-shaped fall back to a plain {@apilink Dictionary}.
37+ * Derives a route map (label → `userData` type) from a {@apilink RouteSchemas} map by inferring each
38+ * schema's output type. Outputs that are not object-shaped fall back to a plain {@apilink Dictionary}.
3639 */
3740export type RoutesFromSchemas < Schemas extends RouteSchemas > = {
3841 [ Label in keyof Schemas ] : StandardSchemaV1 . InferOutput < Schemas [ Label ] > extends Dictionary
3942 ? StandardSchemaV1 . InferOutput < Schemas [ Label ] >
4043 : Dictionary ;
4144} ;
4245
43- /**
44- * The crawling context received by a route handler, with `request.userData` narrowed to `UserData`.
45- */
46- export type RouterHandlerContext < Context , UserData extends Dictionary > = Omit < Context , 'request' > & {
47- request : LoadedRequest < Request < UserData > > ;
48- } ;
49-
50- /**
51- * The set of labels accepted by {@apilink Router.addHandler}. When the router declares a concrete
52- * {@apilink RouteMap} (e.g. `{ PRODUCT: ...; CATEGORY: ... }`), only those labels (plus symbols) are
53- * allowed — unknown labels become a compile-time error. When the map is left open (the default
54- * `Record<string, ...>`), any string or symbol label is accepted, preserving the original behaviour.
55- */
56- export type RouterLabel < Routes extends Record < keyof Routes , Dictionary > > = string extends keyof Routes
57- ? string | symbol
58- : ( keyof Routes & string ) | symbol ;
59-
6046export interface RouterHandler <
6147 Context extends Omit < RestrictedCrawlingContext , 'enqueueLinks' > = CrawlingContext ,
6248 Routes extends Record < keyof Routes , Dictionary > = Record < string , GetUserDataFromRequest < Context [ 'request' ] > > ,
@@ -137,9 +123,9 @@ export type RouterRoutes<Context, Routes extends Record<keyof Routes, Dictionary
137123 *
138124 * ## Typed labels
139125 *
140- * To get `request.userData` typed per label, declare a { @apilink RouteMap} and pass it as the second
141- * type argument. The label passed to {@apilink Router.addHandler} then drives the type of
142- * `request.userData`, and unknown labels are rejected at compile time:
126+ * To get `request.userData` typed per label, declare a route map and pass it as the second type
127+ * argument. The label passed to {@apilink Router.addHandler} then drives the type of `request.userData`,
128+ * and unknown labels are rejected at compile time:
143129 *
144130 * ```ts
145131 * import { createCheerioRouter, CheerioCrawlingContext } from 'crawlee';
@@ -194,7 +180,7 @@ export class Router<
194180 protected constructor ( ) { }
195181
196182 /**
197- * Registers new route handler for given label. When the router declares a { @apilink RouteMap} , the
183+ * Registers new route handler for given label. When the router declares a route map , the
198184 * `label` is restricted to the declared labels and `request.userData` is typed accordingly.
199185 */
200186 addHandler < Label extends keyof Routes & string > (
@@ -203,8 +189,9 @@ export class Router<
203189 ) : void ;
204190
205191 /**
206- * Registers new route handler for given label, with an explicit `request.userData` type. Use this
207- * overload to type a handler whose label is not part of the router's {@apilink RouteMap}.
192+ * Registers new route handler for given label, explicitly typing `request.userData` via the
193+ * `UserData` type argument. Useful when the router has no declared route map (the open default)
194+ * and you want to type a single handler, or to register a handler under a `symbol` label.
208195 */
209196 addHandler < UserData extends Dictionary = GetUserDataFromRequest < Context [ 'request' ] > > (
210197 label : RouterLabel < Routes > ,
@@ -217,10 +204,11 @@ export class Router<
217204 }
218205
219206 /**
220- * Registers default route handler. By default `request.userData` is typed as the union of all
221- * `userData` shapes declared in the router's {@apilink RouteMap}.
207+ * Registers default route handler. As a fallback it can receive any request (including labels not
208+ * declared in the route map), so `request.userData` defaults to the context's `userData` type
209+ * (loosely typed by default). Pass an explicit `UserData` type argument to narrow it.
222210 */
223- addDefaultHandler < UserData extends Dictionary = Routes [ keyof Routes ] > (
211+ addDefaultHandler < UserData extends Dictionary = GetUserDataFromRequest < Context [ 'request' ] > > (
224212 handler : ( ctx : RouterHandlerContext < Context , UserData > ) => Awaitable < void > ,
225213 ) {
226214 this . validate ( defaultRoute ) ;
@@ -332,12 +320,21 @@ export class Router<
332320 * });
333321 * ```
334322 */
323+ // The handler overloads keep the second type argument backwards compatible. When it is a route map
324+ // (every value is a `Dictionary`) the first overload applies and labels are typed per route. Otherwise
325+ // it fails the `Record<keyof Routes, Dictionary>` constraint and falls through to the second overload,
326+ // where it is treated as the legacy flat `userData` shape shared by all handlers. The third overload
327+ // accepts a Standard Schema per label, inferring the route map and validating `userData` at runtime.
335328 static create <
336329 Context extends Omit < RestrictedCrawlingContext , 'enqueueLinks' > = CrawlingContext ,
337- UserData extends Dictionary = GetUserDataFromRequest < Context [ 'request' ] > ,
338- Routes extends Record < keyof Routes , Dictionary > = Record < string , UserData > ,
330+ Routes extends Record < keyof Routes , Dictionary > = Record < string , GetUserDataFromRequest < Context [ 'request' ] > > ,
339331 > ( routes ?: RouterRoutes < Context , Routes > ) : RouterHandler < Context , Routes > ;
340332
333+ static create <
334+ Context extends Omit < RestrictedCrawlingContext , 'enqueueLinks' > = CrawlingContext ,
335+ UserData extends Dictionary = GetUserDataFromRequest < Context [ 'request' ] > ,
336+ > ( routes ?: RouterRoutes < Context , Record < string , UserData > > ) : RouterHandler < Context , Record < string , UserData > > ;
337+
341338 static create <
342339 Context extends Omit < RestrictedCrawlingContext , 'enqueueLinks' > = CrawlingContext ,
343340 const Schemas extends RouteSchemas = RouteSchemas ,
@@ -357,7 +354,7 @@ export class Router<
357354
358355 for ( const [ label , value ] of Object . entries ( routesOrSchemas ?? { } ) ) {
359356 if ( typeof value === 'function' ) {
360- router . addHandler ( label as keyof Context & string , value as ( ctx : any ) => Awaitable < void > ) ;
357+ router . addHandler ( label , value as ( ctx : any ) => Awaitable < void > ) ;
361358 } else {
362359 router . schemas . set ( label , value ) ;
363360 }
0 commit comments