Skip to content

Instantly share code, notes, and snippets.

@david-plugge
Last active August 24, 2024 14:59
Show Gist options
  • Save david-plugge/28c71a9c1a91c09893370d4d0fd89545 to your computer and use it in GitHub Desktop.
Save david-plugge/28c71a9c1a91c09893370d4d0fd89545 to your computer and use it in GitHub Desktop.
Sveltekit typesafe routes
import { resolveRoute } from '$app/paths';
import type RouteMetadata from '$lib/../../.svelte-kit/types/route_meta_data.json';
type RouteMetadata = typeof RouteMetadata;
// eslint-disable-next-line @typescript-eslint/ban-types
type Prettify<T> = { [K in keyof T]: T[K] } & {};
type ParseParam<T extends string> = T extends `...${infer Name}` ? Name : T;
type ParseParams<T extends string> = T extends `${infer A}[[${infer Param}]]${infer B}`
? ParseParams<A> & { [K in ParseParam<Param>]?: string } & ParseParams<B>
: T extends `${infer A}[${infer Param}]${infer B}`
? ParseParams<A> & { [K in ParseParam<Param>]: string } & ParseParams<B>
: // eslint-disable-next-line @typescript-eslint/ban-types
{};
type RequiredKeys<T extends object> = keyof {
// eslint-disable-next-line @typescript-eslint/ban-types
[P in keyof T as {} extends Pick<T, P> ? never : P]: 1;
};
type RemoveGroups<T> = T extends `${infer A}/(${string})${infer B}` ? `${A}${RemoveGroups<B>}` : T;
export type RouteId = RemoveGroups<keyof RouteMetadata>;
// export type RouteId = keyof RouteMetadata;
export type Routes = {
[K in RouteId]: Prettify<ParseParams<K>>;
};
type OptionalOptions<T extends RouteId> = {
query?: string | Record<string, string> | URLSearchParams | string[][];
hash?: string;
params?: Routes[T];
};
type RequiredOptions<T extends RouteId> = {
query?: string | Record<string, string> | URLSearchParams | string[][];
hash?: string;
params: Routes[T];
};
type RouteArgs<T extends RouteId> =
RequiredKeys<Routes[T]> extends never
? [options?: OptionalOptions<T>]
: [options: RequiredOptions<T>];
export function route<T extends RouteId>(routeId: T, ...[options]: RouteArgs<T>) {
const path = resolveRoute(routeId, options?.params ?? {});
const search = options?.query && new URLSearchParams(options.query).toString();
return path + (search ? `?${search}` : '') + (options?.hash ? `#${options.hash}` : '');
}
@Lootwig
Copy link

Lootwig commented Jun 23, 2024

I'm still playing with this - basically nerd-sniped myself, thinking I might find a cool application for tagged template literals but more for the sake of using them than necessity :D

however, just wanted to quickly point out that in case of a root-level group existing (e.g. /(admin)), this RouteId also matches the empty string - maybe not always intended, at least in my case it's not.

Went down another short rabbit hole on whether there's a smarter way, but ended up with the pragmatic approach of

type RemoveGroups<T> = Exclude<
  T extends `${infer A}/(${string})${infer B}` ? `${A}${RemoveGroups<B>}` : T,
  ''
>;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment