@@ -156,10 +156,17 @@ interface IdFn {
156156 ( v : string [ ] ) : string [ ]
157157}
158158
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+
160163const PATH_PARAM_DEFAULT_SET = ( value : unknown ) =>
161164 value && Array . isArray ( value ) ? value . map ( String ) : String ( value )
162165// 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+ }
163170
164171/**
165172 * 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<
201208 repeat ?: boolean
202209 // TODO: not needed because in the regexp, the value is undefined if the group is optional and not given
203210 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
205225}
206226
207227const IS_INTEGER_RE = / ^ - ? [ 1 - 9 ] \d * $ /
@@ -238,43 +258,53 @@ export const PARAM_NUMBER_REPEATABLE_OPTIONAL = {
238258 value != null ? PARAM_NUMBER_REPEATABLE . set ( value ) : null ,
239259} satisfies Param_GetSet < string [ ] | null , number [ ] | null >
240260
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 >
243269
244270 constructor (
245271 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 > > ,
250277 // A better version could be using all the parts to join them
251278 // .e.g ['users', 0, 'profile', 1] -> /users/123/profile/456
252279 // numbers are indexes of the params in the params object keys
253280 readonly pathParts : Array < string | number >
254281 ) {
255- this . paramsKeys = Object . keys ( this . params )
282+ this . paramsKeys = Object . keys ( this . params ) as Array < keyof TParamsOptions >
256283 }
257284
258- match ( path : string ) : MatcherParamsFormatted {
285+ match ( path : string ) : ExtractParamTypeFromOptions < TParamsOptions > {
259286 const match = path . match ( this . re )
260287 if ( ! match ) {
261288 throw miss ( )
262289 }
263290 // 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
269296
270- const value = paramOptions . repeat
297+ var value = paramOptions . repeat
271298 ? ( currentMatch ?. split ( '/' ) || [ ] ) . map (
272- // using just decode makes the type inference fail
299+ // using just decode makes the type inference fail
273300 v => decode ( v )
274301 )
275302 : decode ( currentMatch )
276303
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+ )
278308 }
279309
280310 if (
@@ -289,22 +319,76 @@ export class MatcherPatternPathCustomParams implements MatcherPatternPath {
289319 return params
290320 }
291321
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+ )
305343 }
306344}
307345
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+
308392/**
309393 * Matcher for dynamic paths, e.g. `/team/:id/:name`.
310394 * Supports one, one or zero, one or more and zero or more params.
0 commit comments