react-router-typesafe-routes
Version:
Enhanced type safety via validation for all route params in React Router v7.
204 lines (203 loc) • 15.1 kB
TypeScript
import { PathnameType, SearchType, StateType, HashType } from "../types/index.js";
type Route<TSpec extends RouteSpec = RouteSpec, TChildren = {}> = RouteApi<TSpec> & RouteChildren<TSpec, TChildren> & {
$: RouteChildren<OmitPath<TSpec>, TChildren>;
};
type RouteChildren<TSpec extends RouteSpec, TChildren> = {
[TKey in keyof TChildren]: TChildren[TKey] extends Route<infer TChildOptions, infer TChildChildren> ? Route<MergeRouteSpecList<[TSpec, TChildOptions], "inherit">, TChildChildren> : TChildren[TKey];
};
type PathPattern<TPath extends PathConstraint> = {
<TRelative extends boolean = false>(opts?: {
relative?: TRelative;
}): TRelative extends true ? PathWithoutIntermediateStars<TPath> : AbsolutePath<TPath>;
};
interface RouteApi<TSpec extends RouteSpec = RouteSpec> {
$path: PathPattern<TSpec["path"]>;
$buildPath: (opts: BuildPathOptions<TSpec>) => string;
$buildPathname: (opts: BuildPathnameOptions<TSpec>) => string;
$buildSearch: (opts: BuildSearchOptions<TSpec>) => string;
$buildHash: (opts: BuildHashOptions<TSpec>) => string;
$buildState: (opts: BuildStateOptions<TSpec>) => PlainState<TSpec["state"]>;
$serializeParams: (opts: SerializePathnameParamsOptions<TSpec>) => PathnameParams;
$serializeSearchParams: (opts: BuildSearchOptions<TSpec>) => URLSearchParams;
$deserializeParams: (params: PathnameParams) => OutPathnameParams<TSpec>;
$deserializeSearchParams: (searchParams: URLSearchParams) => OutSearchParams<TSpec>;
$deserializeHash: (hash: string) => OutHash<TSpec>;
$deserializeState: (state: unknown) => OutState<TSpec>;
$spec: TSpec;
}
type PathnameParams = Record<string, string | undefined>;
type BuildPathOptions<TSpec extends RouteSpec> = Readable<InPathParams<TSpec> & PathnameBuilderOptions & SearchBuilderOptions>;
type BuildPathnameOptions<TSpec extends RouteSpec> = Readable<SerializePathnameParamsOptions<TSpec> & PathnameBuilderOptions>;
type SerializePathnameParamsOptions<TSpec extends RouteSpec> = {
params: InPathnameParams<TSpec>;
};
type BuildSearchOptions<TSpec extends RouteSpec> = Readable<{
searchParams: InSearchParams<TSpec>;
} & SearchBuilderOptions>;
type BuildHashOptions<TSpec extends RouteSpec> = Readable<{
hash: InHash<TSpec>;
}>;
type BuildStateOptions<TSpec extends RouteSpec> = Readable<{
state: InState<TSpec>;
} & StateBuilderOptions>;
interface PathnameBuilderOptions {
relative?: boolean;
}
interface SearchBuilderOptions {
untypedSearchParams?: URLSearchParams | undefined;
}
interface StateBuilderOptions {
untypedState?: unknown;
}
type PlainState<TState extends StateConstraint> = TState extends StateObjectConstraint ? Record<string, unknown> : unknown;
type InPathParams<TSpec extends RouteSpec> = Readable<(PathnameParamsRequired<InPathnameParams<TSpec>> extends true ? {
params: InPathnameParams<TSpec>;
} : {
params?: InPathnameParams<TSpec>;
}) & {
searchParams?: InSearchParams<TSpec>;
hash?: InHash<TSpec>;
}>;
type PathnameParamsRequired<T> = Partial<T> extends T ? (IsAny<T> extends true ? true : false) : true;
type InPathnameParams<TSpec extends RouteSpec> = Merge<InferredPathnameParams<TSpec["path"]>, TSpec["params"]> extends infer TResolvedPathnameParams ? TResolvedPathnameParams extends PathnameParamsConstraint ? IsAny<TResolvedPathnameParams> extends true ? any : Merge<Pick<RawParams<TResolvedPathnameParams, "in">, PathParam<TSpec["path"], "all", "in">>, PartialWithUndefined<Pick<RawParams<TResolvedPathnameParams, "in">, PathParam<TSpec["path"], "optional", "in">>>> : never : never;
type OutPathnameParams<TSpec extends RouteSpec> = Merge<InferredPathnameParams<TSpec["path"]>, TSpec["params"]> extends infer TResolvedPathnameParams ? TResolvedPathnameParams extends PathnameParamsConstraint ? Readable<UndefinedToPartial<undefined extends TSpec["path"] ? RawParams<TResolvedPathnameParams, "out"> : Pick<RawParams<TResolvedPathnameParams, "out">, PathParam<TSpec["path"]>>>> : never : never;
type InSearchParams<TSpec extends RouteSpec> = IsAny<TSpec["searchParams"]> extends true ? any : Readable<PartialWithUndefined<RawSearchParams<TSpec["searchParams"], "in">>>;
type OutSearchParams<TSpec extends RouteSpec> = Readable<UndefinedToPartial<RawSearchParams<TSpec["searchParams"], "out">>>;
type InHash<TSpec extends RouteSpec> = RawHash<TSpec["hash"], "in">;
type OutHash<TSpec extends RouteSpec> = RawHash<TSpec["hash"], "out">;
type InState<TSpec extends RouteSpec> = IsAny<TSpec["state"]> extends true ? any : TSpec["state"] extends StateObjectConstraint ? Readable<PartialWithUndefined<RawStateParams<TSpec["state"], "in">>> : TSpec["state"] extends StateUnknownConstraint ? RawState<TSpec["state"], "in"> : never;
type OutState<TSpec extends RouteSpec> = TSpec["state"] extends StateObjectConstraint ? Readable<UndefinedToPartial<RawStateParams<TSpec["state"], "out">>> : TSpec["state"] extends StateUnknownConstraint ? RawState<TSpec["state"], "out"> : never;
type InferredPathnameParams<TPath extends PathConstraint> = Merge<Record<PathParam<TPath>, PathnameType<string>>, Record<PathParam<TPath, "optional">, PathnameType<string | undefined>>>;
type RawParams<TPathnameParams extends PathnameParamsConstraint, TMode extends "in" | "out"> = {
[TKey in keyof TPathnameParams]: TPathnameParams[TKey] extends PathnameType<infer TOut, infer TIn> ? TMode extends "in" ? Exclude<TIn, undefined> : TOut : never;
};
type RawSearchParams<TSearchParams extends SearchParamsConstraint, TMode extends "in" | "out"> = {
[TKey in keyof TSearchParams]: TSearchParams[TKey] extends SearchType<infer TOut, infer TIn> ? TMode extends "in" ? Exclude<TIn, undefined> : TOut : never;
};
type RawState<TState extends StateUnknownConstraint, TMode extends "in" | "out"> = TState extends StateType<infer TOut, infer TIn> ? TMode extends "in" ? Exclude<TIn, undefined> : TOut : never;
type RawHash<THash, TMode extends "in" | "out"> = THash extends string[] ? TMode extends "in" ? [THash[number]] extends [never] ? undefined : THash[number] : THash[number] | undefined : THash extends HashType<infer TOut, infer TIn> ? TMode extends "in" ? Exclude<TIn, undefined> : TOut : undefined;
type RawStateParams<TState extends StateObjectConstraint, TMode extends "in" | "out"> = {
[TKey in keyof TState]: TState[TKey] extends StateType<infer TOut, infer TIn> ? TMode extends "in" ? Exclude<TIn, undefined> : TOut : never;
};
type AbsolutePath<T extends PathConstraint> = T extends string ? `/${T}` : T;
type PathWithoutIntermediateStars<T extends PathConstraint> = T extends `${infer TStart}*?/${infer TEnd}` ? PathWithoutIntermediateStars<`${TStart}${TEnd}`> : T extends `${infer TStart}*/${infer TEnd}` ? PathWithoutIntermediateStars<`${TStart}${TEnd}`> : T;
type SanitizePath<T> = T extends `/${string}` ? ErrorMessage<"Leading slashes are forbidden"> : T extends `${string}/` ? ErrorMessage<"Trailing slashes are forbidden"> : T extends `${string}*?${string}` ? ErrorMessage<"Splats can't be optional"> : T;
type SanitizeRouteChildren<T> = Readable<{
[TKey in keyof T]: TKey extends Omit$<TKey> ? T[TKey] extends RouteApi ? T[TKey] : RouteApi : ErrorMessage<"Name can't start with $">;
}>;
type SanitizePathnameTypes<TPath extends PathConstraint, TPathnameParams> = {
[TKey in keyof TPathnameParams]: TKey extends PathParam<TPath> ? TPathnameParams[TKey] extends undefined ? PathnameType<any> : TPathnameParams[TKey] : ErrorMessage<"There is no such param in path">;
};
type Omit$<T> = T extends `$${infer TValid}` ? TValid : T;
type PathParam<TPath extends PathConstraint, TKind extends "all" | "optional" = "all", TMode extends "in" | "out" = "out"> = string extends TPath ? never : TPath extends `${infer TBefore}*${infer TAfter}` ? ExtractPathParam<"*", TKind, TMode, TAfter extends "" ? true : false> | PathParam<TBefore, TKind, TMode> | PathParam<TAfter, TKind, TMode> : TPath extends `${infer _TStart}:${infer TParam}/${infer TRest}` ? ExtractPathParam<TParam, TKind, TMode> | PathParam<TRest, TKind, TMode> : TPath extends `${infer _TStart}:${infer TParam}` ? ExtractPathParam<TParam, TKind, TMode> : never;
type ExtractPathParam<TRawParam extends string, TKind extends "all" | "optional" = "all", TMode extends "in" | "out" = "out", TEnd extends boolean = false> = TRawParam extends `${infer TParam}?` ? OmitIllegalStar<TParam, TMode, TEnd> : TKind extends "optional" ? TRawParam extends "*" ? TMode extends "in" ? OmitIllegalStar<TRawParam, TMode, TEnd> : never : never : OmitIllegalStar<TRawParam, TMode, TEnd>;
type OmitIllegalStar<TParam extends string, TMode extends "in" | "out" = "out", TEnd extends boolean = false> = TParam extends "*" ? (TMode extends "in" ? (TEnd extends false ? never : TParam) : TParam) : TParam;
type RouteOptions<TPath extends PathConstraint = undefined, TPathnameParams extends Partial<Record<PathParam<TPath>, PathnameType<any>>> = Partial<Record<PathParam<TPath>, PathnameType<any>>>, TSearchParams extends SearchParamsConstraint = {}, THashString extends string = string, THash extends HashConstraint<THashString> = [], TState extends StateConstraint = {}, TComposedRoutes extends [...RouteApi<RouteSpec<undefined>>[]] = [], TChildren = {}> = {
/**
* A path pattern, just like in React Router. The only difference is that leading and trailing slashes are
* forbidden.
*/
path?: SanitizePath<TPath>;
/**
* Pathname params. Use a record of types to override params
* inferred from `path`, partially or completely:
*
* ```ts
* params: { id: number() }
* ```
*
* If there is no `path`, you can specify any params.
*
* */
params?: TPath extends undefined ? PathnameParamsConstraint : SanitizePathnameTypes<TPath, TPathnameParams>;
/**
* Search params. Use a record of types to define them:
*
* ```ts
* searchParams: { page: number() }
* ```
* */
searchParams?: TSearchParams;
/**
* Hash. Use a type to define it:
*
* ```ts
* hash: union(["info", "stats"])
* ```
*
* If you want to extend it in a child route, specify it as an array of string values instead:
*
* ```ts
* hash: ["info", "stats"]
* ```
*/
hash?: THash;
/**
* State. Use a record of types to define it:
*
* ```ts
* state: { expired: boolean() }
* ```
*
* As an escape hatch, you can use a single type (not recommended):
*
* ```ts
* state: boolean()
* ```
*/
state?: TState;
/** An array of pathless routes whose params will be composed into the route. */
compose?: [...TComposedRoutes];
/** Child routes that will inherit all params. */
children?: SanitizeRouteChildren<TChildren>;
};
interface RouteSpec<TPath extends PathConstraint = PathConstraint, TPathnameParams extends PathnameParamsConstraint = any, TSearchParams extends SearchParamsConstraint = any, THash extends HashConstraint = HashConstraint, TState extends StateConstraint = any> {
path: TPath;
params: TPathnameParams;
searchParams: TSearchParams;
hash: THash;
state: TState;
}
type PathConstraint = string | undefined;
type PathnameParamsConstraint = Record<string, PathnameType<any>>;
type SearchParamsConstraint = Record<string, SearchType<any>>;
type StateConstraint = StateObjectConstraint | StateUnknownConstraint;
type StateObjectConstraint = Record<string, StateType<any>>;
type StateUnknownConstraint = StateType<any>;
type HashConstraint<T extends string = string> = T[] | HashType<any>;
type ExtractRouteSpecList<TTuple extends [...RouteApi[]]> = {
[TIndex in keyof TTuple]: TTuple[TIndex]["$spec"];
};
type MergeRouteSpecList<T extends RouteSpec[], TMode extends "inherit" | "compose"> = T extends [
infer TFirst,
infer TSecond,
...infer TRest
] ? TRest extends RouteSpec[] ? MergeRouteSpecList<[MergeRouteSpecPair<TFirst, TSecond, TMode>, ...TRest], TMode> : never : T extends [infer TFirst] ? TFirst extends RouteSpec ? TFirst : never : never;
type MergeRouteSpecPair<T, U, TMode extends "inherit" | "compose"> = T extends RouteSpec<infer TPath, infer TPathnameParams, infer TSearchParams, infer THash, infer TState> ? U extends RouteSpec<infer TChildPath, infer TChildPathnameParams, infer TChildSearchParams, infer TChildHash, infer TChildState> ? RouteSpec<TMode extends "inherit" ? StringPath<TPath> extends "" ? TChildPath : StringPath<TChildPath> extends "" ? TPath : `${TPath}/${TChildPath}` : TChildPath, Merge<TPathnameParams, TChildPathnameParams>, Merge<TSearchParams, TChildSearchParams>, TChildHash extends string[] ? (THash extends string[] ? [...THash, ...TChildHash] : THash) : TChildHash, TChildState extends StateObjectConstraint ? TState extends StateObjectConstraint ? Merge<TState, TChildState> : TState : TChildState> : never : never;
type StringPath<T extends PathConstraint> = T extends undefined ? "" : T;
type OmitPath<T extends RouteSpec> = T extends RouteSpec<infer _TPath, infer TPathnameParams, infer TSearchParams, infer THash, infer TState> ? RouteSpec<"", TPathnameParams, TSearchParams, THash, TState> : never;
type Merge<T, U> = Readable<Omit<T, keyof U> & U>;
type Readable<T> = T extends object ? (T extends infer O ? {
[K in keyof O]: O[K];
} : never) : T;
type ErrorMessage<T extends string> = T & {
[brand]: ErrorMessage<T>;
};
declare const brand: unique symbol;
type IsAny<T> = 0 extends 1 & T ? true : false;
type UndefinedToPartial<T> = Merge<T, Undefined<T>>;
type Undefined<T> = {
[K in keyof T as undefined extends T[K] ? K : never]?: Exclude<T[K], undefined>;
};
type PartialWithUndefined<T> = {
[K in keyof T]?: T[K] | undefined;
};
type NormalizePathnameParams<TPathnameParams, TPath extends PathConstraint> = Partial<Record<PathParam<TPath>, PathnameType<any>>> extends TPathnameParams ? {} : Readable<RequiredWithoutUndefined<TPathnameParams>>;
type RequiredWithoutUndefined<T> = {
[P in keyof T]-?: Exclude<T[P], undefined>;
};
declare function route<TPath extends PathConstraint = undefined, TPathnameParams extends Partial<Record<PathParam<TPath>, PathnameType<any>>> = Partial<Record<PathParam<TPath>, PathnameType<any>>>, TSearchParams extends SearchParamsConstraint = {}, THashString extends string = string, THash extends HashConstraint<THashString> = [], TState extends StateConstraint = {}, TComposedRoutes extends [...RouteApi<RouteSpec<undefined>>[]] = [], TChildren = {}>(opts: RouteOptions<TPath, TPathnameParams, TSearchParams, THashString, THash, TState, TComposedRoutes, TChildren>): Route<MergeRouteSpecList<[
...ExtractRouteSpecList<TComposedRoutes>,
RouteSpec<TPath, NormalizePathnameParams<TPathnameParams, TPath>, TSearchParams, THash, TState>
], "compose">, SanitizeRouteChildren<TChildren>>;
export { route, RouteOptions, Route, RouteApi, RouteChildren, RouteSpec, MergeRouteSpecList, PathParam, SanitizePath, SanitizeRouteChildren, InPathParams, InPathnameParams, OutPathnameParams, InSearchParams, OutSearchParams, InHash, OutHash, InState, OutState, ErrorMessage, };