React Router Typed Params

The useParams and useSearchParams hooks from React Router are critical when working with dynamic routes, however they have some sharp edges when used with TypeScript. With a small amount of additional code, we can make it much easier to work with params.

The two primary issues with useParams (and useSearchParams) are:

  1. All params are marked as potentially undefined, even if they are required params.
  2. All params are returned as strings.

Let’s solve these two problems by creating a custom useStrictParams hook which will both mark params as required as well as handle type conversion for us.

import { useParams, useSearchParams } from "react-router-dom"

type TypeDef = Record<string, NumberConstructor | StringConstructor>

type StrictParams<T> = {
  [K in keyof T]: T[K] extends NumberConstructor ? number
  : T[K] extends StringConstructor ? string
  : never
}

function toStrict<T extends TypeDef>(
  params: [string, string | undefined][],
  typedef: T,
): StrictParams<T> {
  return params.reduce((acc, [key, value]) => {
    const type = typedef[key]
    return type ? { ...acc, [key]: value ? type(value) : null } : acc
  }, {} as StrictParams<T>)
}

export function useStrictParams<T extends TypeDef>(
  typedef: T,
): StrictParams<T> {
  const params = useParams()
  return toStrict(Object.entries(params), typedef)
}

To use this new hook, we can simply call it and provide an object which specifies the types of our params. We cannot simply use generic types since the types are not only used to specify the available params, but in the case of numbers, they also convert the runtime values to the correct types. While this example only uses strings and numbers, adding support for booleans or even dates would be very simple.

const { userId, age } = useStrictParams({ userId: String, age: Number })

We can do the same with search params with the following code:

export function useStrictSearchParams<T extends TypeDef>(
  typedef: T,
): [StrictParams<T>, (params: URLSearchParams) => void] {
  const [searchParams, setSearchParams] = useSearchParams()
  return [toStrict([...searchParams.entries()], typedef), setSearchParams]
}

Future Ideas

This idea could be expanded on to include other features such as converting arrays of search params via the Array type, or even allowing undefined values by creating a Optional type like this:

useStrictParams({ query: Optional(String) })