pbeshai / use-query-params

React Hook for managing state in URL query parameters with easy serialization.
https://pbeshai.github.io/use-query-params
ISC License
2.15k stars 96 forks source link

TypeScript setQuery typing for custom parameter types #200

Closed glensomerville closed 2 years ago

glensomerville commented 2 years ago

I have a custom parameter defined as:

const sortableColumns = [
  'name',
  'status',
  'createdDate',
] as const;

type SortBy = typeof sortableColumns[number];

const SortParam = {
  encode: (sortBy: string): SortBy =>
    (sortableColumns as ReadonlyArray<string>).includes(sortBy)
      ? (sortBy as unknown as SortBy)
      : 'createdDate',
  decode: (sortBy: string | (string | null)[] | null | undefined): SortBy =>
    (
      sortableColumns as ReadonlyArray<string | (string | null)[] | null | undefined>
    ).includes(sortBy)
      ? (sortBy as unknown as SortBy)
      : 'createdDate',

When attempting to call setQuery from the useQueryParams hook with the following sorting function:

const [{ sort }, setQuery] = useQueryParams({
    sort: withDefault(SortParam, 'name', false),
  });

const onSortChange = (sort: string) => {
  setQuery({ sort }); // Type error
};

I get the error Type 'string' is not assignable to type '"name" | "status" | "createdDate"'.

This seems to be due to the setQuery function expecting a value of type DecodedValueMap which expects the type of sort to be SortBy. However, I would expect it to accept the type EncodedValueMap when setting the query value allowing for sort to be of type string as expected when passed to the encode function of the custom parameter map.

Is this a bug, or am I perhaps using it wrong?

pbeshai commented 2 years ago

The expectation is you are working with values in their normal usage form and the library handles the encoding/decoding for you. So you basically always work with a decoded value in your code (outside of creating a custom param). e.g. for an ObjectParam, you'd do:

setQuery({ obj: { foo: 'bar' } })

not

setQuery({ obj: 'foo-bar' })

The same principle applies here.

Also, for your particular use case, you may find createEnumParam handy for generating your SortParam.

glensomerville commented 2 years ago

Thanks for the clarification. This helps šŸ‘

Using the createEnumParam seems to do the job without so much customisation as you said. However, when I attempt to use it together with withDefault to avoid nulls, the type still contains null | undefined.

e.g. if I use the above example with createEnumParam and withDefault:

sortBy: withDefault(createEnumParam<SortBy>([... sortableColumns]), 'createdDate', false),

the type of sortBy remains 'name' | 'status' | 'createdDate' | null | undefined. I can work around it however.

Perhaps there's a bug with the createEnumParam and withDefault combination though?

pbeshai commented 2 years ago

Ah yes this is a weird thing I have never figured out how to get the types to work effortlessly with. You need to use as const after your default value:

sortBy: withDefault(createEnumParam<SortBy>([... sortableColumns]), 'createdDate' as const, false),

This will make sortBy have type 'name' | 'status' | 'createdDate' | null (you passed false meaning you want nulls, default is nulls are excluded with withDefault)

glensomerville commented 2 years ago

Awesome, this solved it! Thanks šŸ‘