My code includes a type called ExtractParams
that extracts parameters from a URL string:
type ExtractParams<Path extends string> = Path extends `${infer Start}(${infer Rest})`
? ExtractParams<Start> & Partial<ExtractParams<Rest>>
: Path extends `${infer Start}/:${infer Param}/${infer Rest}`
? ExtractParams<Start> & ExtractParams<Rest> & { [Key in Param]: string }
: Path extends `${infer Start}/:${infer Param}`
? ExtractParams<Start> & { [Key in Param]: string }
: {};
The purpose of the ExtractParams
type is to convert dynamic route parameters into an object with the parameter names as keys and string values. If a route parameter is optional, the generated object will reflect this by marking that key as optional with a value of string | undefined
.
Here are some examples of using the type:
type RP1 = ExtractRouteParams<'/courses/:courseId/classes/:classId'>;
// ^? { courseId: string; } & { classId: string }
type RP2 = ExtractRouteParams<'/courses/:courseId/classes(/:classId)'>;
// ^? { courseId: string; } & { classId?: string | undefined }
To make the resulting object type cleaner and easier to read, I used a utility type obtained from this question, which merges the intersection of object types:
type Expand<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;
By applying the Expand
utility, I was able to improve the readability of the type:
type Params<Path extends string> = Expand<ExtractParams<Path>>;
type X1 = Params<'/courses/:courseId/classes/:classId'>
// ^? { classId: string; courseId: string }
type X2 = Params<'/courses/:courseId/classes(/:classId)'>
// ^? { classId?: string | undefined; courseId: string }
In summary, the code functions correctly when defining optional parameters in the format a(/:b)
.
I am looking to minimize repetition in the type declaration and focus on the syntax for declaring optional params as a(/:b)
. If there is a solution that accommodates multiple optional param syntaxes, it would be beneficial for future use.
For my specific use case, paths can have multiple optional parameters but will always be separated by at least one required parameter. Even if a solution allows for multiple optional parameters consecutively, it will not impact me negatively.
Valid examples of paths containing optional parameters include:
'/courses(/:courseId)/classes/:classId' - courseId is optional
'/courses/:courseId/classes(/:classId)' - classId is optional
'/courses(/:courseId)/classes(/:classId)' - courseId and classId both are optional
'/courses(/:courseId)(/:classes)(/:classId)' - Additional scenarios are welcomed, but not mandatory.
Invalid examples that I am certain won't be present in my codebase include paths like these:
'(/courses/:courseId)/classes/:classId' - Optional params should not have two slashes
'/courses(/:courseId/classes)/:classId'
Feel free to explore the Playground Link