diff --git a/src/hooks/router.ts b/src/hooks/router.ts index 4504c3e4..cb83f6b3 100644 --- a/src/hooks/router.ts +++ b/src/hooks/router.ts @@ -1,11 +1,22 @@ import { useNavigate as _useNavigate, - useSearchParams, + useParams as _useParams, + useSearchParams as _useSearchParams, type NavigateOptions, type To as NavigateTo, + type Params, } from "react-router-dom" +import { object as objectSchema, type ObjectShape } from "yup" import { type PageState } from "../components/page" +import { type ReadOnly } from "../utils/router" +import { + tryValidateSync, + type ObjectSchemaFromShape, + type TryValidateSyncOnErrorRT, + type TryValidateSyncOptions, + type TryValidateSyncRT, +} from "../utils/schema" export function useNavigate(): < State extends Record = Record, @@ -17,7 +28,7 @@ export function useNavigate(): < }, ) => void { const navigate = _useNavigate() - const searchParams = useSearchParamEntries() + const searchParams = useSearchParams() return (to, options) => { const { next = true, ..._options } = options || {} @@ -26,6 +37,68 @@ export function useNavigate(): < } } -export function useSearchParamEntries() { - return Object.fromEntries(useSearchParams()[0].entries()) +// ----------------------------------------------------------------------------- +// Use Search Params +// ----------------------------------------------------------------------------- + +export function useSearchParams(): { [k: string]: string } + +export function useSearchParams< + OnErrorRT extends TryValidateSyncOnErrorRT>, + Shape extends ObjectShape = {}, +>( + shape: Shape, + validateOptions?: TryValidateSyncOptions< + ObjectSchemaFromShape, + OnErrorRT + >, +): TryValidateSyncRT, OnErrorRT> + +export function useSearchParams< + OnErrorRT extends TryValidateSyncOnErrorRT>, + Shape extends ObjectShape = {}, +>( + shape?: Shape, + validateOptions?: TryValidateSyncOptions< + ObjectSchemaFromShape, + OnErrorRT + >, +) { + const searchParams = Object.fromEntries(_useSearchParams()[0].entries()) + if (!shape) return searchParams + + return tryValidateSync(searchParams, objectSchema(shape), validateOptions) +} + +// ----------------------------------------------------------------------------- +// Use Params +// ----------------------------------------------------------------------------- + +export function useParams(): ReadOnly> + +export function useParams< + OnErrorRT extends TryValidateSyncOnErrorRT>, + Shape extends ObjectShape = {}, +>( + shape: Shape, + validateOptions?: TryValidateSyncOptions< + ObjectSchemaFromShape, + OnErrorRT + >, +): TryValidateSyncRT, OnErrorRT> + +export function useParams< + OnErrorRT extends TryValidateSyncOnErrorRT>, + Shape extends ObjectShape = {}, +>( + shape?: Shape, + validateOptions?: TryValidateSyncOptions< + ObjectSchemaFromShape, + OnErrorRT + >, +) { + const params = _useParams() + if (!shape) return params + + return tryValidateSync(params, objectSchema(shape), validateOptions) } diff --git a/src/utils/router.ts b/src/utils/router.ts index 4321ac5c..e40f3cfe 100644 --- a/src/utils/router.ts +++ b/src/utils/router.ts @@ -1,5 +1,9 @@ import { generatePath } from "react-router-dom" +export type ReadOnly = { + readonly [P in keyof T]: T[P] +} + export type Parameters = Record export interface Path { diff --git a/src/utils/schema.ts b/src/utils/schema.ts index e2e857c5..aa71d14f 100644 --- a/src/utils/schema.ts +++ b/src/utils/schema.ts @@ -1,52 +1,80 @@ import { - type InferType, - type ValidateOptions, ValidationError, + type AnyObject, + type DefaultFromShape, + type InferType, + type MakePartial, type ObjectSchema, + type ObjectShape, + type Schema, + type TypeFromShape, + type ValidateOptions, } from "yup" -export function tryValidateSync>( +export type _ = T extends {} + ? { + [k in keyof T]: T[k] + } + : T + +export type MakeKeysOptional = T extends AnyObject ? _> : T + +export type ObjectFromShape = MakeKeysOptional< + _> +> + +export type ObjectSchemaFromShape = ObjectSchema< + _>, + AnyObject, + _>, + "" +> + +// ----------------------------------------------------------------------------- +// Try Validate Sync +// ----------------------------------------------------------------------------- + +export type TryValidateSyncOnErrorRT = InferType | void + +export type TryValidateSyncRT< + S extends Schema, + OnErrorRT extends TryValidateSyncOnErrorRT, +> = OnErrorRT extends InferType ? InferType : InferType | undefined + +export type TryValidateSyncOptions< + S extends Schema, + OnErrorRT extends TryValidateSyncOnErrorRT, +> = ValidateOptions & { + onError?: (error: ValidationError) => OnErrorRT +} + +export function tryValidateSync( value: any, - schema: Schema, - kwArgs?: { - options?: ValidateOptions - }, -): InferType | undefined + schema: S, + options?: ValidateOptions, +): InferType | undefined export function tryValidateSync< - Schema extends ObjectSchema, - OnErrorRT extends InferType | void, + S extends Schema, + OnErrorRT extends TryValidateSyncOnErrorRT, >( value: any, - schema: Schema, - kwArgs?: { - options?: ValidateOptions + schema: S, + options?: ValidateOptions & { onError: (error: ValidationError) => OnErrorRT }, -): OnErrorRT extends InferType - ? InferType - : InferType | undefined +): TryValidateSyncRT export function tryValidateSync< - Schema extends ObjectSchema, - OnErrorRT extends InferType | void, ->( - value: any, - schema: Schema, - kwArgs?: { - options?: ValidateOptions - onError?: (error: ValidationError) => OnErrorRT - }, -): InferType | undefined { + S extends Schema, + OnErrorRT extends TryValidateSyncOnErrorRT, +>(value: any, schema: S, options?: TryValidateSyncOptions) { + const { onError, ...validateOptions } = options || {} + try { - return schema.validateSync(value, kwArgs?.options) + return schema.validateSync(value, validateOptions) } catch (error) { - if (!(error instanceof ValidationError)) { - throw error - } else if (kwArgs?.onError !== undefined) { - return kwArgs.onError(error) as InferType | undefined - } + if (!(error instanceof ValidationError)) throw error + else if (onError) return onError(error) } - - return undefined }