From 07234e340d05819c7f261e44b3b02fb51a01d01e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Best?= Date: Thu, 26 Dec 2024 10:05:51 +0000 Subject: [PATCH] feat: Add UrlKeys type helper --- packages/nuqs/src/cache.ts | 10 ++--- packages/nuqs/src/defs.ts | 32 +++++++++++++++ packages/nuqs/src/parsers.ts | 5 +++ packages/nuqs/src/serializer.ts | 11 ++---- packages/nuqs/src/tests/cache.test-d.ts | 41 ++++++++++++++++++++ packages/nuqs/src/tests/serializer.test-d.ts | 41 ++++++++++++++++++++ packages/nuqs/src/useQueryStates.ts | 6 +-- 7 files changed, 130 insertions(+), 16 deletions(-) diff --git a/packages/nuqs/src/cache.ts b/packages/nuqs/src/cache.ts index df3df919..22c2c3c7 100644 --- a/packages/nuqs/src/cache.ts +++ b/packages/nuqs/src/cache.ts @@ -1,16 +1,14 @@ // @ts-ignore import { cache } from 'react' -import type { SearchParams } from './defs' +import type { SearchParams, UrlKeys } from './defs' import { error } from './errors' -import type { ParserBuilder, inferParserType } from './parsers' +import type { inferParserType, ParserMap } from './parsers' const $input: unique symbol = Symbol('Input') -export function createSearchParamsCache< - Parsers extends Record> ->( +export function createSearchParamsCache( parsers: Parsers, - { urlKeys = {} }: { urlKeys?: Partial> } = {} + { urlKeys = {} }: { urlKeys?: UrlKeys } = {} ) { type Keys = keyof Parsers type ParsedSearchParams = { diff --git a/packages/nuqs/src/defs.ts b/packages/nuqs/src/defs.ts index 6e67e3eb..db787203 100644 --- a/packages/nuqs/src/defs.ts +++ b/packages/nuqs/src/defs.ts @@ -66,3 +66,35 @@ export type Options = { export type Nullable = { [K in keyof T]: T[K] | null } + +/** + * Helper type to define and reuse urlKey options to rename search params keys + * + * Usage: + * ```ts + * import { type UrlKeys } from 'nuqs' // or 'nuqs/server' + * + * export const coordinatesSearchParams = { + * latitude: parseAsFloat.withDefault(0), + * longitude: parseAsFloat.withDefault(0), + * } + * export const coordinatesUrlKeys: UrlKeys = { + * latitude: 'lat', + * longitude: 'lng', + * } + * + * // Later in the code: + * useQueryStates(coordinatesSearchParams, { + * urlKeys: coordinatesUrlKeys + * }) + * createSerializer(coordinatesSearchParams, { + * urlKeys: coordinatesUrlKeys + * }) + * createSearchParamsCache(coordinatesSearchParams, { + * urlKeys: coordinatesUrlKeys + * }) + * ``` + */ +export type UrlKeys> = Partial< + Record +> diff --git a/packages/nuqs/src/parsers.ts b/packages/nuqs/src/parsers.ts index 9a94681b..1bcd3a78 100644 --- a/packages/nuqs/src/parsers.ts +++ b/packages/nuqs/src/parsers.ts @@ -464,3 +464,8 @@ export type inferParserType = : Input extends Record> ? inferParserRecordType : never + +export type ParserWithOptionalDefault = ParserBuilder & { + defaultValue?: T +} +export type ParserMap = Record> diff --git a/packages/nuqs/src/serializer.ts b/packages/nuqs/src/serializer.ts index bcb8665f..9ea77f4f 100644 --- a/packages/nuqs/src/serializer.ts +++ b/packages/nuqs/src/serializer.ts @@ -1,19 +1,16 @@ -import type { Nullable, Options } from './defs' -import type { inferParserType, ParserBuilder } from './parsers' +import type { Nullable, Options, UrlKeys } from './defs' +import type { inferParserType, ParserMap } from './parsers' import { renderQueryString } from './url-encoding' type Base = string | URLSearchParams | URL -type ParserWithOptionalDefault = ParserBuilder & { defaultValue?: T } -export function createSerializer< - Parsers extends Record> ->( +export function createSerializer( parsers: Parsers, { clearOnDefault = true, urlKeys = {} }: Pick & { - urlKeys?: Partial> + urlKeys?: UrlKeys } = {} ) { type Values = Partial>> diff --git a/packages/nuqs/src/tests/cache.test-d.ts b/packages/nuqs/src/tests/cache.test-d.ts index fc9fb7ba..16c53b14 100644 --- a/packages/nuqs/src/tests/cache.test-d.ts +++ b/packages/nuqs/src/tests/cache.test-d.ts @@ -34,3 +34,44 @@ import { expectType>(cache.parse(Promise.resolve({}))) expectType(cache.all()) } + +// It supports urlKeys +{ + createSearchParamsCache( + { + foo: parseAsString, + bar: parseAsInteger + }, + { + urlKeys: { + foo: 'f' + // It accepts partial inputs + } + } + ) + createSearchParamsCache( + { + foo: parseAsString, + bar: parseAsInteger + }, + { + urlKeys: { + foo: 'f', + bar: 'b' + } + } + ) + expectError(() => { + createSearchParamsCache( + { + foo: parseAsString, + bar: parseAsInteger + }, + { + urlKeys: { + nope: 'n' // Doesn't accept extra properties + } + } + ) + }) +} diff --git a/packages/nuqs/src/tests/serializer.test-d.ts b/packages/nuqs/src/tests/serializer.test-d.ts index f07eb521..9e21cba1 100644 --- a/packages/nuqs/src/tests/serializer.test-d.ts +++ b/packages/nuqs/src/tests/serializer.test-d.ts @@ -61,3 +61,44 @@ import { createSerializer, parseAsInteger, parseAsString } from '../../dist' expectType(serialize({ bar: null })) expectType(serialize({ bar: undefined })) } + +// It supports urlKeys +{ + createSerializer( + { + foo: parseAsString, + bar: parseAsInteger + }, + { + urlKeys: { + foo: 'f' + // It accepts partial inputs + } + } + ) + createSerializer( + { + foo: parseAsString, + bar: parseAsInteger + }, + { + urlKeys: { + foo: 'f', + bar: 'b' + } + } + ) + expectError(() => { + createSerializer( + { + foo: parseAsString, + bar: parseAsInteger + }, + { + urlKeys: { + nope: 'n' // Doesn't accept extra properties + } + } + ) + }) +} diff --git a/packages/nuqs/src/useQueryStates.ts b/packages/nuqs/src/useQueryStates.ts index 0fb7d744..23a81ff0 100644 --- a/packages/nuqs/src/useQueryStates.ts +++ b/packages/nuqs/src/useQueryStates.ts @@ -8,12 +8,12 @@ import { } from 'react' import { useAdapter } from './adapters/lib/context' import { debug } from './debug' -import type { Nullable, Options } from './defs' +import type { Nullable, Options, UrlKeys } from './defs' import type { Parser } from './parsers' import { emitter, type CrossHookSyncPayload } from './sync' import { - FLUSH_RATE_LIMIT_MS, enqueueQueryStringUpdate, + FLUSH_RATE_LIMIT_MS, getQueuedValue, scheduleFlushToURL } from './update-queue' @@ -30,7 +30,7 @@ export type UseQueryStatesKeysMap = { export type UseQueryStatesOptions = Options & { - urlKeys: Partial> + urlKeys: UrlKeys } export type Values = {