Skip to content

Commit

Permalink
feat: Add UrlKeys type helper
Browse files Browse the repository at this point in the history
  • Loading branch information
franky47 committed Dec 26, 2024
1 parent 8951598 commit 07234e3
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 16 deletions.
10 changes: 4 additions & 6 deletions packages/nuqs/src/cache.ts
Original file line number Diff line number Diff line change
@@ -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<string, ParserBuilder<any>>
>(
export function createSearchParamsCache<Parsers extends ParserMap>(
parsers: Parsers,
{ urlKeys = {} }: { urlKeys?: Partial<Record<keyof Parsers, string>> } = {}
{ urlKeys = {} }: { urlKeys?: UrlKeys<Parsers> } = {}
) {
type Keys = keyof Parsers
type ParsedSearchParams = {
Expand Down
32 changes: 32 additions & 0 deletions packages/nuqs/src/defs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,35 @@ export type Options = {
export type Nullable<T> = {
[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<typeof coordinatesSearchParams> = {
* latitude: 'lat',
* longitude: 'lng',
* }
*
* // Later in the code:
* useQueryStates(coordinatesSearchParams, {
* urlKeys: coordinatesUrlKeys
* })
* createSerializer(coordinatesSearchParams, {
* urlKeys: coordinatesUrlKeys
* })
* createSearchParamsCache(coordinatesSearchParams, {
* urlKeys: coordinatesUrlKeys
* })
* ```
*/
export type UrlKeys<Parsers extends Record<string, any>> = Partial<
Record<keyof Parsers, string>
>
5 changes: 5 additions & 0 deletions packages/nuqs/src/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,3 +464,8 @@ export type inferParserType<Input> =
: Input extends Record<string, ParserBuilder<any>>
? inferParserRecordType<Input>
: never

export type ParserWithOptionalDefault<T> = ParserBuilder<T> & {
defaultValue?: T
}
export type ParserMap = Record<string, ParserWithOptionalDefault<any>>
11 changes: 4 additions & 7 deletions packages/nuqs/src/serializer.ts
Original file line number Diff line number Diff line change
@@ -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<T> = ParserBuilder<T> & { defaultValue?: T }

export function createSerializer<
Parsers extends Record<string, ParserWithOptionalDefault<any>>
>(
export function createSerializer<Parsers extends ParserMap>(
parsers: Parsers,
{
clearOnDefault = true,
urlKeys = {}
}: Pick<Options, 'clearOnDefault'> & {
urlKeys?: Partial<Record<keyof Parsers, string>>
urlKeys?: UrlKeys<Parsers>
} = {}
) {
type Values = Partial<Nullable<inferParserType<Parsers>>>
Expand Down
41 changes: 41 additions & 0 deletions packages/nuqs/src/tests/cache.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,44 @@ import {
expectType<Promise<All>>(cache.parse(Promise.resolve({})))
expectType<All>(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
}
}
)
})
}
41 changes: 41 additions & 0 deletions packages/nuqs/src/tests/serializer.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,44 @@ import { createSerializer, parseAsInteger, parseAsString } from '../../dist'
expectType<string>(serialize({ bar: null }))
expectType<string>(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
}
}
)
})
}
6 changes: 3 additions & 3 deletions packages/nuqs/src/useQueryStates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -30,7 +30,7 @@ export type UseQueryStatesKeysMap<Map = any> = {

export type UseQueryStatesOptions<KeyMap extends UseQueryStatesKeysMap> =
Options & {
urlKeys: Partial<Record<keyof KeyMap, string>>
urlKeys: UrlKeys<KeyMap>
}

export type Values<T extends UseQueryStatesKeysMap> = {
Expand Down

0 comments on commit 07234e3

Please sign in to comment.