react-router: Possible useParams TypeScript broken definition
import { useParams } from "react-router-dom"
interface JobPageRouteParams {
jobId: string
}
const MyComponent = () => {
const { jobId } = useParams<JobPageRouteParams>();
}
gives an error: Type 'JobPageRouteParams' does not satisfy the constraint 'string'. .
The definition on useParams is
export declare function useParams<Key extends string = string>(): Readonly<Params<Key>>;
which means that the generic type is supposed to be a string?? Am I missing something here?
About this issue
- Original URL
- State: closed
- Created 3 years ago
- Reactions: 24
- Comments: 30 (3 by maintainers)
Commits related to this issue
- Profile: useParams() — committed to camelcrush/insta-web by camelcrush 2 years ago
- fix(vite): preserve names for exports from .client imports (#8200) Co-authored-by: Hiroshi Ogawa <hi.ogawa.zz@gmail.com> — committed to remix-run/react-router by pcattori 7 months ago
The rationale here for us, as I see it (and I’ve talked to Michael about it a bunch and believe he’s of the same mind):
string | undefinedfor values is technically correctlet { slug } = useParams() as { slug: string }patch-packageand override our types in a postinstall hookI didn’t want to scatter my code with the non-null assertion operator, so I decided to go with this. Equally bad (or good 😉), but I only need this declaration once inside a component instead of everywhere where the variable is used:
This keeps coming up. The thing to remember is that the user in charge of the URL and you can put a
useParamsanywhere you want in your code.There is no way for TypeScript, or React Router, to know for sure that a certain param is actually in the URL. It requires a runtime check because your code does not control the value, the URL (at runtime) does.
Best we can do is let you say “I only expect these params” but that doesn’t mean they are actually going to be there, because, again, those values come from the URL at runtime, not your code.
Would love to be wrong.
Bring in
tiny-invariantand add the runtime check:Thank you, I was using old tutorials for v5 where the definition was different, and got tunnel vision. I see I can use string literals now.
On another note: v6 now returns the value of a param as
string | undefined. What is the correct approach to using the value of a param? Doing a null check seems counterproductive and does not align well with function components. Using the non-null assertion operator seems kinda hacky. Am I missing the better approach?Hi @chaance. I wanna talk about your points about the desition of using Partial.
You said: “Current type signature is simpler to type and output of string | undefined for values is technically correct”. Yes, it is technically correct, but technically doesn’t mean fully. As this is right now, the only one that knows what is actually correct is the developer, and thus, the only one that should be able to describe this fully is the developer.
After that you mention:
The reason why the non-null assertion operator is a language feature is to allow JS to move to TS easily, by taking the risk. We don’t have any risk here, if I know that my param needs to be set in order to be able to show the page, I don’t need to use the non-null assertion operator, this is an anti-pattern in most cases, and its usage is not recommended unless really needed (again, not the case if I can describe the whole thing properly). eslint rule
Enhancing code that should work out the box sounds overkill for this.
Turning off strict null check is not an option. That’s an architectural choice that should be internal to be taken.
I understand that you think this is the best to really express what this means. However, I think it would be better for everyone to document the cases where using a partial would make sense, and let the user decide what shape they want to use.
I ended up using the non-null assertion operator when I want to tell TypeScript to shut up. So
Yes, but we can now use it like this:
Those
undefinedparams are bugging me.I understand that in theory you could access some param key that’s not within the route:
If you write code like this, then a check for
bar !== undefinedwon’t fix your code, instead it will just make it compile without errors and this will hide the underlying problem.Though I’d vote to remove the
undefined.As an alternative you could leave the choice to the user which behavior he prefers, with something like this:
2nd’ing @michaeljota’s points - options 1 and 3 are both steps in the wrong direction as far as TS is concerned, and declaring your own module as a workaround seems very odd when the rationale is “it’s simpler to type”.
Also like @michaeljota said, in my use case
string | undefinedis objectively incorrect. There is no logical way to route toItemDetailsPagewith an undefined itemId.Route:
<Route path="/item-details/:itemId" element={<ItemDetailsPage />} />Hook:const { itemId } = useParams<'itemId'>();.Previously we could use the
renderprop to access route props, which had knowledge about the params as specified by thepath. This was excellent, because I got autocompletion of the params and I didn’t have to manually specify them or perform runtime checks. I’m migrating from v5 to v6 and I’m going to miss this.This is very informative. I wrote a hook that uses some ideas here. Thanks for having more foresight than I did.
Hello here 👋 ,
After reading some discussions and workarounds here, I thought it would be interesting to share a different approach to parameter typing using
useParams. Maybe the parameters should be typed according to the path used in the parentRoutecomponent. PerhapsuseParamscould take the path of the route as a parameter so that the params types are inferred from the path itself instead of using a dedicated list. The use of the path can be bound to the component and written in only one place by declaring it as an attribute of the component.Better code than words, here is how this new hook named
useRouteParamslooks like:Below are some examples of the result:
Then, the component that is bound to a path:
And finally the usage of this component in the router configuration:
The fact that the component is linked to a single path could also be considered as a negative point as it makes the component less reusable. But this seems less error prone and a workaround could be to create a simple wrapper and use shared components inside it.
This approach does the job we’ve been waiting for, but I’d be curious to know what you think 🙂 . @chaance I guess this could also be one more solution for the list of workarounds you made above 🙃.
I’m also clearly open to making a pull request if that’s desired here otherwise we plan to put this in a dedicated NPM package.
I ended up with such solution:
This correctly resolves types and avoid using non-null assertion operator
No, you can’t. See here for example.
The issue is that we are telling to the router we know what is the shape of the params, and the router is ignoring this information and providing optional typing on top on that anyway. (And that for some reason we can’t provide an interface as an argument).
@chaance I believe this is kind of bug that will bite you for years to come.
It can be undefined elsewhere, but using it elsewhere would be a mistake on the dev side. Params might be
string | undefinedin general, but whenuseParamsgets a type explicitly, likeuseParams<SomeType>it should just use that type - that’s what everybody expects I guess. As you might have noticed by reactions to the comment that you reference, none of the solutions are fully acceptable, moreover they are actually a hackarounds.In order to access a param in the URL, you need to give useParams a proper type argument:
const { jobId } = useParams<{ jobId: string}>();The point, as Ryan mentioned, is that you can call
useParamsanywhere in your code, not just the route module itself. Sure it’ll always be defined in that module but elsewhere you’ll have no such guarantee. It is possible that you may getundefinedso the signature is correct, and folks who want the strictest type safety should get it.You’re welcome to keep adding your 👎 to this comment, but it outlines solutions that are totally reasonable and in-line with how TypeScript is designed to work. All of these are TypeScript features for a reason.
I’d also like to ask that you understand that a library cannot make everyone happy all of the time, so what we doing is the technically correct thing. Best we can do for all of the others is offer guidance, which I think we’ve done here.
I think the react-router team is doing the right thing with the type definition here, and a runtime check is the right move. It makes your code more explicit, more robust to change, and easier to debug.
For a project I was doing at work, I wrote a useRequiredParams hook which does the runtime check and returns param variables with type string. I wrote it up here in case anyone wants to re-use. I opened a feature request here in case the maintainers see value in adding it (or something like it).
@chaance What is the point of
undefinedoption in there? If param was undefined that Route wouldn’t even load, right? So I don’t get the point.I don’t think that’s solving something, but actually exposing why TS should be about “what this should be at run time” instead of “what it is at code time”. Using casting like this is just telling TS “you have to trust me on this”, and that’s why TS is useful. Having to use casting like this because the package doesn’t want to trust the author of the code is overkill.
When I need to declare the type of the value, the current way cannot be achieved
Even if it must be a string, in ts, I can declare ‘aa’ |‘bb’ |‘cc’
Before i can
There will be smart prompts when using params.type in this way
Not now, but this problem can be solved by function overloading
In the code
The best of both worlds! If possible, I can submit a pr
(The returned P is processed by Partial, which is consistent with the current logic)
[ParamsOrKey] extends [string]is to avoid Distributive Conditional Types@michaeljota completely agree! Casting like this defeats the purpose of TS, and that’s why I added a warning to my suggestion with
equally bad. What casting offers us is convenience at the cost of type safety. It’s the developer’s responsibility to decide whether the compromise is worth it.@q-kirill Seems very verbose. What’s the advantage of declaring an interface over inlining the type?
But you don’t know that at compile time. TypeScript isn’t aware of how the History API or the internals of React Router work. It can’t enforce that for you, even if you have it that way logically. For example, you could be unit testing your ItemDetailsPage component without wrapping it in a Route
The idiomatic way of doing this is to add type guards for
params. That’s obviously pretty verbose in most cases, so we provide the generic with the safest case of what we know can exist inparamsat compile time.@ryanflorence if you get asked this question a lot, perhaps you should include it in your examples where useParams is used.
@tombuntus Neat solution. I’m not a TypeScript guru, but I think it can be cleaned up a bit using Type Guards.