Skip to content

Instantly share code, notes, and snippets.

@markmals
Last active November 20, 2025 20:44
Show Gist options
  • Select an option

  • Save markmals/d4ffd9dcc410f4244af64e604beda798 to your computer and use it in GitHub Desktop.

Select an option

Save markmals/d4ffd9dcc410f4244af64e604beda798 to your computer and use it in GitHub Desktop.
import type { RoutePattern } from "@remix-run/route-pattern";
// MARK: Types
interface RouteMap {
[key: string]: Route<any> | RouteMap;
}
interface Route<Pattern extends string> extends RoutePattern<Pattern> {
children?: RouteMap;
}
declare function route<Pattern extends string>(pattern: Pattern): Route<Pattern>;
declare function index(): Route<"/">;
declare const LAYOUT: unique symbol;
// Pathless layout component
declare function layout<Children extends RouteMap | undefined>(
children: Children
): Children & { [LAYOUT]: true };
// Split "path?search" into path/search pieces
type PathPart<S extends string> = S extends `${infer P}?${string}` ? P : S;
type SearchPart<S extends string> = S extends `${string}?${infer Q}` ? `?${Q}` : "";
// Join two paths, avoiding "//"
type JoinPaths<A extends string, B extends string> = A extends `${infer A2}/`
? B extends `/${infer B2}`
? `${A2}/${B2}`
: `${A}${B}`
: `${A}${B}`;
// Compose two full patterns, pushing all "?..." to the end
type JoinPattern<Base extends string, Child extends string> = `${JoinPaths<
PathPart<Base>,
PathPart<Child>
>}${SearchPart<Base>}${SearchPart<Child>}`;
type RouteObject = Record<string, any>;
// Rewrite every Route<P> inside T as Route<JoinPattern<Base, P>>
// and recursively descend into plain objects.
type WithBase<Base extends string, T> = T extends Route<infer P>
? Route<JoinPattern<Base, P>>
: T extends RouteObject
? { [K in keyof T]: WithBase<Base, T[K]> }
: T;
declare function prefix<Base extends string, const Children extends Record<string, any>>(
pattern: Base,
children: Children
): WithBase<Base, Children>;
declare function route<Base extends string, const Children extends Record<string, any>>(
base: Base,
children: Children
): Route<Base> & WithBase<Base, Children>;
export const ResourceMethods = ["new", "show", "edit"] as const;
export type ResourceMethod = (typeof ResourceMethods)[number];
export interface ResourceOptions {
/**
* The resource methods to include in the route map. If not provided, all
* methods (`show`, `new`, and `edit`) will be
* included.
*/
only?: ResourceMethod[];
/**
* Custom names to use for the resource routes.
*/
names?: {
new?: string;
show?: string;
edit?: string;
};
}
type ResourceMethodsFor<Options extends ResourceOptions | undefined> = Options extends {
only: readonly (infer M)[];
}
? M & ResourceMethod
: ResourceMethod;
type ResourceKeyFor<
M extends ResourceMethod,
Options extends ResourceOptions | undefined
> = Options extends { names: infer N }
? N extends Record<string, string>
? M extends keyof N
? N[M] & string
: M
: M
: M;
type ResourceRouteFor<Base extends string, M extends ResourceMethod> = M extends "new"
? Route<`${Base}/new`>
: M extends "show"
? Route<Base>
: M extends "edit"
? Route<`${Base}/edit`>
: never;
type ResourceMap<Base extends string, Options extends ResourceOptions | undefined> = {
[M in ResourceMethodsFor<Options> as ResourceKeyFor<M, Options>]: ResourceRouteFor<Base, M>;
};
declare function resource<Base extends string, const Options extends ResourceOptions>(
base: Base | RoutePattern<Base>,
options?: Options
): ResourceMap<Base, Options>;
export const ResourcesMethods = ["index", "new", "show", "edit"] as const;
export type ResourcesMethod = (typeof ResourcesMethods)[number];
export type ResourcesOptions = {
/**
* The resource methods to include in the route map. If not provided, all
* methods (`index`, `show`, `new`, `edit`)
* will be included.
*/
only?: ResourcesMethod[];
/**
* The parameter name to use for the resource. Defaults to `id`.
*/
param?: string;
/**
* Custom names to use for the resource routes.
*/
names?: {
index?: string;
new?: string;
show?: string;
edit?: string;
};
};
type ResourcesMethodsFor<Options extends ResourcesOptions | undefined> = Options extends {
only: readonly (infer M)[];
}
? M & ResourcesMethod
: ResourcesMethod;
type ResourcesKeyFor<
M extends ResourcesMethod,
Options extends ResourcesOptions | undefined
> = Options extends { names: infer N }
? N extends Record<string, string>
? M extends keyof N
? N[M] & string
: M
: M
: M;
type ResourcesParamFor<Options extends ResourcesOptions | undefined> = Options extends {
param: infer P extends string;
}
? P
: "id";
type ResourcesRouteFor<
Base extends string,
Param extends string,
M extends ResourcesMethod
> = M extends "index"
? Route<Base>
: M extends "new"
? Route<`${Base}/new`>
: M extends "show"
? Route<`${Base}/:${Param}`>
: M extends "edit"
? Route<`${Base}/:${Param}/edit`>
: never;
type ResourcesMap<Base extends string, Options extends ResourcesOptions | undefined> = {
[M in ResourcesMethodsFor<Options> as ResourcesKeyFor<M, Options>]: ResourcesRouteFor<
Base,
ResourcesParamFor<Options>,
M
>;
};
declare function resources<Base extends string, const Options extends ResourcesOptions>(
base: Base | RoutePattern<Base>,
options?: Options
): ResourcesMap<Base, Options>;
// MARK: Example
let routes = route("/?q", {
index: index(),
contacts: prefix("/contacts", {
show: route("/:contactId"),
edit: route("/:contactId/edit"),
}),
});
// Outputs the types:
//
// let routes: Route<"/?q"> & {
// readonly index: Route<"/?q">;
// readonly contacts: {
// readonly show: Route<"/contacts/:contactId?q">;
// readonly edit: Route<"/contacts/:contactId/edit?q">;
// };
// };
routes.href(null, { q: "Mark" });
routes.index.href();
routes.contacts.show.href({ contactId: "1" });
routes.contacts.edit.href({ contactId: "1" });
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment