@@ -156,10 +156,17 @@ interface IdFn {
156
156
( v : string [ ] ) : string [ ]
157
157
}
158
158
159
- const PATH_PARAM_DEFAULT_GET = ( value => value ?? null ) as IdFn
159
+ const PATH_PARAM_DEFAULT_GET = ( value : string | string [ ] | null | undefined ) =>
160
+ value ?? null
161
+ export const PATH_PARAM_SINGLE_DEFAULT : Param_GetSet < string , string > = { }
162
+
160
163
const PATH_PARAM_DEFAULT_SET = ( value : unknown ) =>
161
164
value && Array . isArray ( value ) ? value . map ( String ) : String ( value )
162
165
// TODO: `(value an null | undefined)` for types
166
+ export const PATH_PARAM_DEFAULT_PARSER : Param_GetSet = {
167
+ get : PATH_PARAM_DEFAULT_GET ,
168
+ set : PATH_PARAM_DEFAULT_SET ,
169
+ }
163
170
164
171
/**
165
172
* NOTE: I tried to make this generic and infer the types from the params but failed. This is what I tried:
@@ -201,7 +208,20 @@ interface MatcherPatternPathCustomParamOptions<
201
208
repeat ?: boolean
202
209
// TODO: not needed because in the regexp, the value is undefined if the group is optional and not given
203
210
optional ?: boolean
204
- parser ?: Param_GetSet < TIn , TOut >
211
+ parser : Param_GetSet < TIn , TOut >
212
+ }
213
+
214
+ /**
215
+ * Helper type to extract the params from the options object.
216
+ * @internal
217
+ */
218
+ type ExtractParamTypeFromOptions < TParamsOptions > = {
219
+ [ K in keyof TParamsOptions ] : TParamsOptions [ K ] extends MatcherPatternPathCustomParamOptions <
220
+ any ,
221
+ infer TOut
222
+ >
223
+ ? TOut
224
+ : never
205
225
}
206
226
207
227
const IS_INTEGER_RE = / ^ - ? [ 1 - 9 ] \d * $ /
@@ -238,43 +258,53 @@ export const PARAM_NUMBER_REPEATABLE_OPTIONAL = {
238
258
value != null ? PARAM_NUMBER_REPEATABLE . set ( value ) : null ,
239
259
} satisfies Param_GetSet < string [ ] | null , number [ ] | null >
240
260
241
- export class MatcherPatternPathCustomParams implements MatcherPatternPath {
242
- private paramsKeys : string [ ]
261
+ export class MatcherPatternPathCustomParams <
262
+ TParamsOptions ,
263
+ // TODO: | EmptyObject ?
264
+ // TParamsOptions extends Record<string, MatcherPatternPathCustomParamOptions>,
265
+ // TParams extends MatcherParamsFormatted = ExtractParamTypeFromOptions<TParamsOptions>
266
+ > implements MatcherPatternPath < ExtractParamTypeFromOptions < TParamsOptions > >
267
+ {
268
+ private paramsKeys : Array < keyof TParamsOptions >
243
269
244
270
constructor (
245
271
readonly re : RegExp ,
246
- readonly params : Record <
247
- string ,
248
- MatcherPatternPathCustomParamOptions < unknown , unknown >
249
- > ,
272
+ // NOTE: this version instead of extends allows the constructor
273
+ // to properly infer the types of the params when using `new MatcherPatternPathCustomParams()`
274
+ // otherwise, we need to use a factory function: https://github.com/microsoft/TypeScript/issues/40451
275
+ readonly params : TParamsOptions &
276
+ Record < string , MatcherPatternPathCustomParamOptions < any , any > > ,
250
277
// A better version could be using all the parts to join them
251
278
// .e.g ['users', 0, 'profile', 1] -> /users/123/profile/456
252
279
// numbers are indexes of the params in the params object keys
253
280
readonly pathParts : Array < string | number >
254
281
) {
255
- this . paramsKeys = Object . keys ( this . params )
282
+ this . paramsKeys = Object . keys ( this . params ) as Array < keyof TParamsOptions >
256
283
}
257
284
258
- match ( path : string ) : MatcherParamsFormatted {
285
+ match ( path : string ) : ExtractParamTypeFromOptions < TParamsOptions > {
259
286
const match = path . match ( this . re )
260
287
if ( ! match ) {
261
288
throw miss ( )
262
289
}
263
290
// NOTE: if we have params, we assume named groups
264
- const params = { } as MatcherParamsFormatted
265
- let i = 1 // index in match array
266
- for ( const paramName in this . params ) {
267
- const paramOptions = this . params [ paramName ]
268
- const currentMatch = ( match [ i ] as string | undefined ) ?? null
291
+ const params = { } as ExtractParamTypeFromOptions < TParamsOptions >
292
+ for ( var i = 0 ; i < this . paramsKeys . length ; i ++ ) {
293
+ var paramName = this . paramsKeys [ i ]
294
+ var paramOptions = this . params [ paramName ]
295
+ var currentMatch = ( match [ i + 1 ] as string | undefined ) ?? null
269
296
270
- const value = paramOptions . repeat
297
+ var value = paramOptions . repeat
271
298
? ( currentMatch ?. split ( '/' ) || [ ] ) . map (
272
- // using just decode makes the type inference fail
299
+ // using just decode makes the type inference fail
273
300
v => decode ( v )
274
301
)
275
302
: decode ( currentMatch )
276
303
277
- params [ paramName ] = ( paramOptions . parser ?. get || ( v => v ) ) ( value )
304
+ params [ paramName ] = ( paramOptions . parser ?. get || ( v => v ) ) (
305
+ value
306
+ // NOTE: paramName and paramOptions are not connected from TS point of view
307
+ )
278
308
}
279
309
280
310
if (
@@ -289,22 +319,76 @@ export class MatcherPatternPathCustomParams implements MatcherPatternPath {
289
319
return params
290
320
}
291
321
292
- build ( params : MatcherParamsFormatted ) : string {
293
- return this . pathParts . reduce ( ( acc , part ) => {
294
- if ( typeof part === 'string' ) {
295
- return acc + '/' + part
296
- }
297
- const paramName = this . paramsKeys [ part ]
298
- const paramOptions = this . params [ paramName ]
299
- const value = ( paramOptions . parser ?. set || ( v => v ) ) ( params [ paramName ] )
300
- const encodedValue = Array . isArray ( value )
301
- ? value . map ( encodeParam ) . join ( '/' )
302
- : encodeParam ( value )
303
- return encodedValue ? acc + '/' + encodedValue : acc
304
- } , '' )
322
+ build ( params : ExtractParamTypeFromOptions < TParamsOptions > ) : string {
323
+ return (
324
+ '/' +
325
+ this . pathParts
326
+ . map ( part => {
327
+ if ( typeof part === 'string' ) {
328
+ return part
329
+ }
330
+ const paramName = this . paramsKeys [ part ]
331
+ const paramOptions = this . params [ paramName ]
332
+ const value : ReturnType < NonNullable < Param_GetSet [ 'set' ] > > = (
333
+ paramOptions . parser ?. set || ( v => v )
334
+ ) ( params [ paramName ] )
335
+
336
+ return Array . isArray ( value )
337
+ ? value . map ( encodeParam ) . join ( '/' )
338
+ : encodeParam ( value )
339
+ } )
340
+ . filter ( Boolean )
341
+ . join ( '/' )
342
+ )
305
343
}
306
344
}
307
345
346
+ const aaa = new MatcherPatternPathCustomParams (
347
+ / ^ \/ p r o f i l e s \/ ( [ ^ / ] + ) $ / i,
348
+ {
349
+ userId : {
350
+ parser : PARAM_INTEGER ,
351
+ // parser: PATH_PARAM_DEFAULT_PARSER,
352
+ } ,
353
+ } ,
354
+ [ 'profiles' , 0 ]
355
+ )
356
+ // @ts -expect-error: not existing param
357
+ aaa . build ( { a : '2' } )
358
+ // @ts -expect-error: must be a number
359
+ aaa . build ( { userId : '2' } )
360
+ aaa . build ( { userId : 2 } )
361
+ // @ts -expect-error: not existing param
362
+ aaa . match ( '/profiles/2' ) ?. e
363
+ // @ts -expect-error: not existing param
364
+ aaa . match ( '/profiles/2' ) . e
365
+ aaa . match ( '/profiles/2' ) . userId . toFixed ( 2 )
366
+
367
+ // Factory function for better type inference
368
+ export function createMatcherPatternPathCustomParams <
369
+ TParamsOptions extends Record <
370
+ string ,
371
+ MatcherPatternPathCustomParamOptions < any , any >
372
+ > ,
373
+ > (
374
+ re : RegExp ,
375
+ params : TParamsOptions ,
376
+ pathParts : Array < string | number >
377
+ ) : MatcherPatternPathCustomParams < TParamsOptions > {
378
+ return new MatcherPatternPathCustomParams ( re , params , pathParts )
379
+ }
380
+
381
+ // Now use it like this:
382
+ const aab = createMatcherPatternPathCustomParams (
383
+ / ^ \/ p r o f i l e s \/ ( [ ^ / ] + ) $ / i,
384
+ {
385
+ userId : {
386
+ parser : PARAM_INTEGER ,
387
+ } ,
388
+ } ,
389
+ [ 'profiles' , 0 ]
390
+ )
391
+
308
392
/**
309
393
* Matcher for dynamic paths, e.g. `/team/:id/:name`.
310
394
* Supports one, one or zero, one or more and zero or more params.
0 commit comments