Skip to content

Commit

Permalink
feat: Add literal parsers (#453)
Browse files Browse the repository at this point in the history
* feat: Add parseAsStringConst

* feat: Add parseAsLiteral

* feat: Split parseAsLiteral into parseAsStringLiteral and parseAsNumberLiteral

* doc: Update parseAsNumberLiteral default value
  • Loading branch information
timheerwagen authored Jan 12, 2024
1 parent aed4723 commit 7040976
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 7 deletions.
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ import {
parseAsIsoDateTime,
parseAsArrayOf,
parseAsJson,
parseAsStringEnum
parseAsStringEnum,
parseAsStringLiteral,
parseAsNumberLiteral
} from 'nuqs'

useQueryState('tag') // defaults to string
Expand All @@ -128,6 +130,24 @@ const [direction, setDirection] = useQueryState(
parseAsStringEnum<Direction>(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:
Expand Down Expand Up @@ -594,7 +614,7 @@ export function Server() {

// client.tsx
// prettier-ignore
;'use client'
'use client'

import { useQueryStates } from 'nuqs'
import { coordinatesParsers } from './searchParams'
Expand Down
22 changes: 21 additions & 1 deletion packages/docs/content/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ import {
parseAsIsoDateTime,
parseAsArrayOf,
parseAsJson,
parseAsStringEnum
parseAsStringEnum,
parseAsStringLiteral,
parseAsNumberLiteral
} from 'nuqs'

useQueryState('tag') // defaults to string
Expand All @@ -119,6 +121,24 @@ const [direction, setDirection] = useQueryState(
parseAsStringEnum<Direction>(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:
Expand Down
75 changes: 71 additions & 4 deletions packages/nuqs/src/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -206,9 +206,8 @@ export const parseAsIsoDateTime = createParser({
*
* const [direction, setDirection] = useQueryState(
* 'direction',
* queryTypes
* .stringEnum<Direction>(Object.values(Direction))
* .withDefault(Direction.up)
* parseAsStringEnum<Direction>(Object.values(Direction)) // pass a list of allowed values
* .withDefault(Direction.up)
* )
* ```
*
Expand All @@ -230,6 +229,74 @@ export function parseAsStringEnum<Enum extends string>(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<Literal extends string>(
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<Literal extends number>(
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.
Expand Down

0 comments on commit 7040976

Please sign in to comment.