UNPKG

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
import { PathnameType, SearchType, StateType, HashType } from "../types/index.mjs"; 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, };