47ng / nuqs

Type-safe search params state manager for React frameworks - Like useState, but stored in the URL query string.
https://nuqs.47ng.com
MIT License
4.75k stars 104 forks source link

parseAsArrayOf for readonly T[]? #751

Open alula opened 1 week ago

alula commented 1 week ago

Context

What's your version of nuqs?

"nuqs": "^2.1.1"

What framework are you using?

Which version of your framework are you using?

"next": "^15.0.2",
"react": "^18.3.1",
"react-dom": "^18.3.1"

Description

parseAsArrayOf specifies mutable arrays in it's types, which is slightly annoying if you want to pass readonly T[] into functions like withDefault or returned setState, as they expect you to pass a mutable array type.

I'm not sure if making it readonly would have any downsides.

Reproduction

import { useQueryState } from 'nuqs';
import { parseAsArrayOf, parseAsStringLiteral } from 'nuqs/parsers';

// ....

const TYPE_OF_PLATFORM = ['web', 'ios', 'android'] as const;
const TYPE_OF_PLATFORM_DEFAULT = ['web'] as const;

const [typeOfPlatform, setTypeOfPlatform] = useQueryState(
  "type",
  parseAsArrayOf(parseAsStringLiteral(TYPE_OF_PLATFORM)).withDefault(
    TYPE_OF_PLATFORM_DEFAULT // type error
  )
);

setTypeOfPlatform(TYPE_OF_PLATFORM_DEFAULT); // type error
franky47 commented 1 week ago

If you're only interested in type-level immutability (as const, readonly keywords), you could define a utility that casts the parser type:

import { type ParserBuilder } from 'nuqs/server'

function parseAsReadonly<T>(parser: ParserBuilder<T>) {
  return parser as ParserBuilder<Readonly<T>>
}

// Usage:
const TYPE_OF_PLATFORM = ['web', 'ios', 'android'] as const
const TYPE_OF_PLATFORM_DEFAULT = ['web'] as const

function usePlatformType() {
  const [typeOfPlatform, setTypeOfPlatform] = useQueryState(
    'type',
    parseAsReadonly(
      parseAsArrayOf(parseAsStringLiteral(TYPE_OF_PLATFORM))
    ).withDefault(
      TYPE_OF_PLATFORM_DEFAULT
    )
  )

  const reset = () => setTypeOfPlatform(TYPE_OF_PLATFORM_DEFAULT)
}

If you want runtime-immutability, you could make that wrapper act like an actual parser that freezes the parsed query string:

import { createParser } from 'nuqs/server'

function parseAsReadonly<T>(parser: ParserBuilder<T>) {
  return createParser<Readonly<T>>({
    ...parser,
    parse(query) {
      const value = parser.parse(query)
      if (value === null) {
        return null
      }
      return Object.freeze(value)
    },
  })
}