Skip to content

Instantly share code, notes, and snippets.

@terrysahaidak
Created August 29, 2025 12:01
Show Gist options
  • Select an option

  • Save terrysahaidak/9b2ff50034e6018a401f07b13d559157 to your computer and use it in GitHub Desktop.

Select an option

Save terrysahaidak/9b2ff50034e6018a401f07b13d559157 to your computer and use it in GitHub Desktop.
/**
* Type-safe route parameter parser library with pattern-based type inference
*/
// Template literal type utilities for extracting parameter names from route patterns
type ExtractRouteParams<T extends string> = T extends `${infer _Start}:${infer Param}/${infer Rest}`
? {[K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string}
: T extends `${infer _Start}:${infer Param}`
? {[K in Param]: string}
: Record<string, never>;
// Helper type to ensure we get clean parameter extraction
type RouteParams<T extends string> = string extends T
? Record<string, string> // Fallback for non-literal strings
: ExtractRouteParams<T>;
export interface RouteMatch<T extends string = string> {
params: RouteParams<T>;
matched: boolean;
}
/**
* Type-safe route parser that infers parameter types from the pattern
* @param pattern Route pattern like "/users/:userId/posts/:postId"
* @param path Actual URL path like "/users/123/posts/456"
* @returns Typed object containing matched parameters
*/
export function parseRoute<T extends string>(pattern: T, path: string): RouteMatch<T> {
// Normalize paths by removing trailing slashes (except root)
const normalizeUrl = (url: string) => (url === '/' ? '/' : url.replace(/\/$/, ''));
const normalizedPattern = normalizeUrl(pattern);
const normalizedPath = normalizeUrl(path);
// Split into segments
const patternSegments = normalizedPattern.split('/').filter(Boolean);
const pathSegments = normalizedPath.split('/').filter(Boolean);
// Must have same number of segments
if (patternSegments.length !== pathSegments.length) {
return {params: {} as RouteParams<T>, matched: false};
}
const params: Record<string, string> = {};
// Check each segment
for (let i = 0; i < patternSegments.length; i++) {
const patternSegment = patternSegments[i];
const pathSegment = pathSegments[i];
if (patternSegment.startsWith(':')) {
// Parameter segment - extract parameter name and value
const paramName = patternSegment.slice(1);
params[paramName] = decodeURIComponent(pathSegment);
} else if (patternSegment !== pathSegment) {
// Static segment must match exactly
return {params: {} as RouteParams<T>, matched: false};
}
}
return {params: params as RouteParams<T>, matched: true};
}
/**
* Type-safe router class with pattern-based type inference
*/
export class Router {
private routes: Array<{
pattern: string;
handler: (params: Record<string, string>) => any;
}> = [];
/**
* Register a route pattern with a type-safe handler
*/
route<T extends string>(pattern: T, handler: (params: RouteParams<T>) => any): void {
this.routes.push({
pattern,
handler: handler as (params: Record<string, string>) => any,
});
}
/**
* Match a path against registered routes and call the first matching handler
*/
match(path: string): any {
for (const {pattern, handler} of this.routes) {
const result = parseRoute(pattern, path);
if (result.matched) {
return handler(result.params);
}
}
return null;
}
/**
* Get all matching routes (useful for middleware scenarios)
*/
matchAll(path: string): Array<{
params: Record<string, string>;
handler: (params: Record<string, string>) => any;
}> {
const matches = [];
for (const {pattern, handler} of this.routes) {
const result = parseRoute(pattern, path);
if (result.matched) {
matches.push({params: result.params, handler});
}
}
return matches;
}
}
/**
* Utility type for creating typed route handlers outside of the Router class
*/
export type RouteHandler<T extends string> = (params: RouteParams<T>) => any;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment