next-safe-navigation
Version:
Type-safe navigation for NextJS App router
152 lines (147 loc) • 8.83 kB
TypeScript
import { z } from 'zod';
import { Route } from 'next';
type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
type ExcludeAny<T> = unknown extends T ? never : T;
type PathBlueprint = `/${string}`;
type Suffix = `?${string}`;
/**
* When `experimental.typeRoutes` is disabled,
* `Route` is `string & {}`, therefore `string extends Route` is a truthy condition.
* If this is the case, we simply use the `Path` value to infer the literal string.
*
* If `experimental.typeRoutes` is enabled,
* `Route` will be a union of string literals, therefore `string extends Route` is a falsy condition.
* If this is the case, we use `Route<Path>` so that we have auto-complete on the available routes
* generated by NextJS and validation check against dynamic routes (that are checked by passing the string generic).
*/
type SafePath<Path extends string> = string extends Route ? Path : Route<Path>;
type ExtractPathParams<T extends string> = T extends `${infer Rest}[[...${infer Param}]]` ? Param | ExtractPathParams<Rest> : T extends `${infer Rest}[...${infer Param}]` ? Param | ExtractPathParams<Rest> : T extends `${string}[${infer Param}]${infer Rest}` ? Param | ExtractPathParams<Rest> : never;
type RouteBuilder<Path extends string, Params extends z.ZodSchema, Search extends z.ZodSchema> = [
Params,
Search
] extends [never, never] ? {
(): Path;
getSchemas: () => {
params: never;
search: never;
};
} : [Params, Search] extends [z.ZodSchema, never] ? {
(options: z.input<Params>): Path;
getSchemas: () => {
params: Params;
search: never;
};
} : [Params, Search] extends [never, z.ZodSchema] ? undefined extends z.input<Search> ? {
(options?: {
search?: z.input<Search>;
}): Path | `${Path}${Suffix}`;
getSchemas: () => {
params: never;
search: Search;
};
} : {
(options: {
search: z.input<Search>;
}): `${Path}${Suffix}`;
getSchemas: () => {
params: never;
search: Search;
};
} : [Params, Search] extends [z.ZodSchema, z.ZodSchema] ? undefined extends z.input<Search> ? {
(options: z.input<Params> & {
search?: z.input<Search>;
}): Path | `${Path}${Suffix}`;
getSchemas: () => {
params: Params;
search: Search;
};
} : {
(options: z.input<Params> & {
search: z.input<Search>;
}): `${Path}${Suffix}`;
getSchemas: () => {
params: Params;
search: Search;
};
} : never;
type EnsurePathWithNoParams<Path extends string> = ExtractPathParams<Path> extends never ? SafePath<Path> : `[ERROR]: Missing validation for path params`;
/**
* Ensures no extra values are passed to params validation
*/
type StrictParams<Schema extends z.ZodSchema, Keys extends string> = Schema extends z.ZodObject<infer Params> ? [
keyof Params
] extends [Keys] ? Schema : z.ZodObject<{
[Key in keyof Params]: Key extends Keys ? Params[Key] : never;
}> : never;
type RouteBuilderResult<Path extends string, PathParams extends string, Params extends z.ZodObject<any>, Search extends z.ZodSchema> = [
PathParams,
Search
] extends [string, never] ? RouteBuilder<Path, Params, never> : [PathParams, Search] extends [never, z.ZodSchema] ? RouteBuilder<Path, never, Search> : [PathParams, Search] extends [string, z.ZodSchema] ? RouteBuilder<Path, Params, Search> : never;
declare function makeRouteBuilder<Path extends PathBlueprint>(path: EnsurePathWithNoParams<Path>): RouteBuilder<Path, never, never>;
declare function makeRouteBuilder<Path extends PathBlueprint, Params extends z.ZodObject<{
[K in ExtractPathParams<Path>]: z.ZodSchema;
}>, Search extends z.ZodSchema = never>(path: SafePath<Path>, schemas: ExtractPathParams<Path> extends never ? {
search: Search | z.ZodOptional<z.ZodSchema>;
} : {
params: StrictParams<Params, ExtractPathParams<Path>>;
search?: Search | z.ZodOptional<z.ZodSchema>;
}): RouteBuilderResult<Path, ExtractPathParams<Path>, ExcludeAny<Params>, ExcludeAny<Search>>;
type makeRouteBuilder = typeof makeRouteBuilder;
type AnyRouteBuilder = RouteBuilder<string, any, any> | RouteBuilder<string, any, never> | RouteBuilder<string, never, any> | RouteBuilder<string, never, never>;
type NavigationConfig = Record<string, AnyRouteBuilder>;
type SafeRootRoute<Path extends string> = () => Path;
type SafeRouteWithParams<Path extends string, Params extends z.ZodSchema> = {
(options: z.input<Params>): Path;
$parseParams: (params: unknown) => z.output<Params>;
};
type SafeRouteWithSearch<Path extends string, Search extends z.ZodSchema> = {
(options?: {
search?: z.input<Search>;
}): Path;
$parseSearchParams: (searchParams: unknown) => z.output<Search>;
};
type SafeRouteWithRequiredSearch<Path extends string, Search extends z.ZodSchema> = {
(options: {
search: z.input<Search>;
}): Path;
$parseSearchParams: (searchParams: unknown) => z.output<Search>;
};
type SafeRouteWithParamsAndSearch<Path extends string, Params extends z.ZodSchema, Search extends z.ZodSchema, Options = z.input<Params> & {
search?: z.input<Search>;
}> = {
(options: Prettify<Options>): Path;
$parseParams: (params: unknown) => z.output<Params>;
$parseSearchParams: (searchParams: unknown) => z.output<Search>;
};
type SafeRouteWithParamsAndRequiredSearch<Path extends string, Params extends z.ZodSchema, Search extends z.ZodSchema, Options = z.input<Params> & {
search: z.input<Search>;
}> = {
(options: Prettify<Options>): Path;
$parseParams: (params: unknown) => z.output<Params>;
$parseSearchParams: (searchParams: unknown) => z.output<Search>;
};
type SafeRoute<Path extends string, Params extends z.ZodSchema, Search extends z.ZodSchema> = [
Params,
Search
] extends [never, never] ? SafeRootRoute<Path> : [Params, Search] extends [z.ZodSchema, never] ? SafeRouteWithParams<Path, Params> : [Params, Search] extends [never, z.ZodSchema] ? undefined extends z.input<Search> ? SafeRouteWithSearch<Path, Search> : SafeRouteWithRequiredSearch<Path, Search> : [Params, Search] extends [z.ZodSchema, z.ZodSchema] ? undefined extends z.input<Search> ? SafeRouteWithParamsAndSearch<Path, Params, Search> : SafeRouteWithParamsAndRequiredSearch<Path, Params, Search> : never;
type RouteWithParams<Config extends NavigationConfig> = {
[Route in keyof Config & string]: Config[Route] extends (RouteBuilder<string, infer Params extends z.ZodSchema, infer _>) ? Params extends z.ZodSchema ? Route : never : never;
}[keyof Config & string];
type RouteWithSearchParams<Config extends NavigationConfig> = {
[Route in keyof Config & string]: Config[Route] extends (RouteBuilder<string, infer _ extends z.ZodSchema, infer Search extends z.ZodSchema>) ? Search extends z.ZodSchema ? Route : never : never;
}[keyof Config & string];
type SafeNavigation<Config extends NavigationConfig> = {
[Route in keyof Config]: Config[Route] extends (RouteBuilder<infer Path extends string, infer Params extends z.ZodSchema, infer Search extends z.ZodSchema>) ? SafeRoute<Path, Params, Search> : never;
};
type ValidatedRouteParams<Config extends NavigationConfig, Route extends string, AcceptableRoute extends string, Router = SafeNavigation<Config>> = Route extends keyof Pick<Router, AcceptableRoute & keyof Router> ? Router[Route] extends (SafeRoute<string, infer Params extends z.ZodSchema, any> | SafeRoute<string, infer Params extends z.ZodSchema, never>) ? z.output<Params> : never : never;
type ValidatedRouteSearchParams<Config extends NavigationConfig, Route extends string, AcceptableRoute extends string, Router = SafeNavigation<Config>> = Route extends keyof Pick<Router, AcceptableRoute & keyof Router> ? Router[Route] extends (SafeRoute<string, any, infer Search extends z.ZodSchema> | SafeRoute<string, never, infer Search extends z.ZodSchema>) ? z.output<Search> : never : never;
interface SafeNavigationConfigImpl<Config extends NavigationConfig, $SafeRouter extends SafeNavigation<any> = SafeNavigation<Config>, $RouteWithParams extends string = RouteWithParams<Config>, $RouteWithSearchParams extends string = RouteWithSearchParams<Config>> {
routes: $SafeRouter;
useSafeParams: <Route extends keyof $SafeRouter & string>(route: Extract<$RouteWithParams, Route>) => ValidatedRouteParams<Config, Route, $RouteWithParams>;
useSafeSearchParams: <Route extends keyof $SafeRouter & string>(route: Extract<$RouteWithSearchParams, Route>) => ValidatedRouteSearchParams<Config, Route, $RouteWithSearchParams>;
}
type SafeNavigationConfig<Config extends NavigationConfig> = SafeNavigationConfigImpl<Config>;
declare function createNavigationConfig<Config extends NavigationConfig>(createConfig: (defineRoute: makeRouteBuilder) => Config): SafeNavigationConfig<Config>;
export { createNavigationConfig };