diff --git a/README.md b/README.md index 4c915047..f7bfb081 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,9 @@ import { parseAsIsoDateTime, parseAsArrayOf, parseAsJson, - parseAsStringEnum + parseAsStringEnum, + parseAsStringLiteral, + parseAsNumberLiteral } from 'nuqs' useQueryState('tag') // defaults to string @@ -128,6 +130,24 @@ const [direction, setDirection] = useQueryState( parseAsStringEnum(Object.values(Direction)) // pass a list of allowed values .withDefault(Direction.up) ) + +// Literals (string-based only) +const colors = ['red', 'green', 'blue'] as const + +const [color, setColor] = useQueryState( + 'color', + parseAsStringLiteral(colors) // pass a readonly list of allowed values + .withDefault('red') +) + +// Literals (number-based only) +const diceSides = [1, 2, 3, 4, 5, 6] as const + +const [side, setSide] = useQueryState( + 'side', + parseAsNumberLiteral(diceSides) // pass a readonly list of allowed values + .withDefault(4) +) ``` You may pass a custom set of `parse` and `serialize` functions: @@ -594,7 +614,7 @@ export function Server() { // client.tsx // prettier-ignore -;'use client' +'use client' import { useQueryStates } from 'nuqs' import { coordinatesParsers } from './searchParams' diff --git a/packages/docs/content/docs/index.mdx b/packages/docs/content/docs/index.mdx index c944f9e5..be9c2ec9 100644 --- a/packages/docs/content/docs/index.mdx +++ b/packages/docs/content/docs/index.mdx @@ -94,7 +94,9 @@ import { parseAsIsoDateTime, parseAsArrayOf, parseAsJson, - parseAsStringEnum + parseAsStringEnum, + parseAsStringLiteral, + parseAsNumberLiteral } from 'nuqs' useQueryState('tag') // defaults to string @@ -119,6 +121,24 @@ const [direction, setDirection] = useQueryState( parseAsStringEnum(Object.values(Direction)) // pass a list of allowed values .withDefault(Direction.up) ) + +// Literals (string-based only) +const colors = ['red', 'green', 'blue'] as const + +const [color, setColor] = useQueryState( + 'color', + parseAsStringLiteral(colors) // pass a readonly list of allowed values + .withDefault('red') +) + +// Literals (number-based only) +const diceSides = [1, 2, 3, 4, 5, 6] as const + +const [side, setSide] = useQueryState( + 'side', + parseAsNumberLiteral(diceSides) // pass a readonly list of allowed values + .withDefault(4) +) ``` You may pass a custom set of `parse` and `serialize` functions: diff --git a/packages/nuqs/src/parsers.ts b/packages/nuqs/src/parsers.ts index 2d3a1c4c..98b68dae 100644 --- a/packages/nuqs/src/parsers.ts +++ b/packages/nuqs/src/parsers.ts @@ -191,7 +191,7 @@ export const parseAsIsoDateTime = createParser({ /** * String-based enums provide better type-safety for known sets of values. - * You will need to pass the stringEnum function a list of your enum values + * You will need to pass the parseAsStringEnum function a list of your enum values * in order to validate the query string. Anything else will return `null`, * or your default value if specified. * @@ -206,9 +206,8 @@ export const parseAsIsoDateTime = createParser({ * * const [direction, setDirection] = useQueryState( * 'direction', - * queryTypes - * .stringEnum(Object.values(Direction)) - * .withDefault(Direction.up) + * parseAsStringEnum(Object.values(Direction)) // pass a list of allowed values + * .withDefault(Direction.up) * ) * ``` * @@ -230,6 +229,74 @@ export function parseAsStringEnum(validValues: Enum[]) { }) } +/** + * String-based literals provide better type-safety for known sets of values. + * You will need to pass the parseAsStringLiteral function a list of your string values + * in order to validate the query string. Anything else will return `null`, + * or your default value if specified. + * + * Example: + * ```ts + * const colors = ["red", "green", "blue"] as const + * + * const [color, setColor] = useQueryState( + * 'color', + * parseAsStringLiteral(colors) // pass a readonly list of allowed values + * .withDefault("red") + * ) + * ``` + * + * @param validValues The values you want to accept + */ +export function parseAsStringLiteral( + validValues: readonly Literal[] +) { + return createParser({ + parse: (query: string) => { + const asConst = query as unknown as Literal + if (validValues.includes(asConst)) { + return asConst + } + return null + }, + serialize: (value: Literal) => value.toString() + }) +} + +/** + * Number-based literals provide better type-safety for known sets of values. + * You will need to pass the parseAsNumberLiteral function a list of your number values + * in order to validate the query string. Anything else will return `null`, + * or your default value if specified. + * + * Example: + * ```ts + * const diceSides = [1, 2, 3, 4, 5, 6] as const + * + * const [side, setSide] = useQueryState( + * 'side', + * parseAsNumberLiteral(diceSides) // pass a readonly list of allowed values + * .withDefault(4) + * ) + * ``` + * + * @param validValues The values you want to accept + */ +export function parseAsNumberLiteral( + validValues: readonly Literal[] +) { + return createParser({ + parse: (query: string) => { + const asConst = parseFloat(query) as unknown as Literal + if (validValues.includes(asConst)) { + return asConst + } + return null + }, + serialize: (value: Literal) => value.toString() + }) +} + /** * Encode any object shape into the querystring value as JSON. * Value is URI-encoded for safety, so it may not look nice in the URL.