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:
- All params are marked as potentially undefined, even if they are required params.
- 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) })