-
-
Notifications
You must be signed in to change notification settings - Fork 10.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Possible useParams TypeScript broken definition #8200
Comments
I looked more into the implementation of type RouteParams<T> = {
readonly [key in keyof T]: string | undefined;
};
export function useParams<T extends Record<string, string>>(): Readonly<RouteParams<T>> {
let { matches } = React.useContext(RouteContext);
let routeMatch = matches[matches.length - 1];
return ( routeMatch ? (routeMatch.params) : {} ) as RouteParams<T>;
} Now, I could use it like this: type JobPageRouteParams = {
jobId: string
};
const { jobId } = useParams<JobPageRouteParams>(); |
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 |
I'm also curious about this: the behavior used to be that an |
Those I understand that in theory you could access some param key that's not within the route: <Route path=":foo" />
const { bar } = useParams();
// bar === undefined If you write code like this, then a check for Though I'd vote to remove the As an alternative you could leave the choice to the user which behavior he prefers, with something like this:
|
I ended up using the non-null assertion operator when I want to tell TypeScript to shut up. So <Route path=":foo" />
const { bar } = useParams<"bar">();
const fetchBar = useBar(bar!) |
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):
|
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". After that you mention:
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. |
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 Route: |
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 |
That makes sense for why react-router can't automatically specify it as required. But then why does react-router make the assumption it could be undefined at compile time? There's no right answer for every possible scenario, which is why I believe @DragosMocrii's original interface proposal makes the most sense. #8200 (comment) If I'm unit testing the component, then I'm just mocking useParams. I'm not changing the type signatures in my production component just to support tests. |
I 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 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 let params = useParams<'userId'>();
invariant(params.userId);
// carry on with type safety and a runtime check from invariant |
I ended up with such solution:
This correctly resolves types and avoid using non-null assertion operator |
@q-kirill Seems very verbose. What's the advantage of declaring an interface over inlining the type?
|
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. |
@michaeljota completely agree! Casting like this defeats the purpose of TS, and that's why I added a warning to my suggestion with |
@chaance What is the point of |
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). |
@tombuntus Neat solution. I'm not a TypeScript guru, but I think it can be cleaned up a bit using Type Guards. |
The point, as Ryan mentioned, is that you can call 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. |
@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 |
Hello here 👋 , After reading some discussions and workarounds here, I thought it would be interesting to share a different approach to parameter typing using Better code than words, here is how this new hook named import React from "react"
import { Route, Routes, useParams } from "react-router-dom"
// The params type doesn't need to be optional anymore since now if a
// param is not detected in a path the related property won't exist at all
type NonNullableKeys<T> = {
[P in keyof T]-?: NonNullable<T[P]>
}
// Transforms the path to params names as a string literal
type PathParamsLiteral<S extends string> = S extends
| `:${infer T}/${infer U}`
| `/:${infer T}/${infer U}`
? T | PathParamsLiteral<U>
: S extends `${infer _T}/${infer U}`
? PathParamsLiteral<U>
: S extends `:${infer T}` | `/:${infer T}`
? T
: never
export const useRouteParams = <RoutePath extends string>(_path: RoutePath) => {
const params = useParams<PathParamsLiteral<RoutePath>>()
return params as NonNullableKeys<typeof params>
} Below are some examples of the result: // a: NonNullableKeys<Readonly<Params<"foo" | "fob" | "bar" | "oba">>>
const a = useRouteParams(":foo/:fob/:bar/:oba/foo/bar")
// b: NonNullableKeys<Readonly<Params<never>>>
const b = useRouteParams("/foo/bar/foobar")
// c: NonNullableKeys<Readonly<Params<"foo">>>
const c = useRouteParams(":foo")
// d: NonNullableKeys<Readonly<Params<"foo">>>
const d = useRouteParams("/:foo")
// e: NonNullableKeys<Readonly<Params<"foo" | "oba">>>
const e = useRouteParams("/:foo/foobar/:oba")
// f: NonNullableKeys<Readonly<Params<"oba">>>
const f = useRouteParams("/foo/bar/:oba/foobar")
// g: NonNullableKeys<Readonly<Params<"oba">>>
const g = useRouteParams("foo/bar/:oba/foobar")
// h: NonNullableKeys<Readonly<Params<never>>>
const h = useRouteParams("foo") Then, the component that is bound to a path: // Route path written only once at one place, living with the component/wrapper using it with useRouteParams
const MY_COMPONENT_PATH = ":foo/:fob/:bar/:oba/foo/bar"
const MyComponent = () => {
const params = useRouteParams(MY_COMPONENT_PATH)
return <p>{params.foo}</p>
}
MyComponent.routePath = MY_COMPONENT_PATH And finally the usage of this component in the router configuration: import React from "react"
import { Route, Routes } from "react-router-dom"
import { MyComponent } from "./MyComponent"
// To be even stricter, a dedicated Route component
// to enforce the path usage from the component
const AppRoute = <T extends Record<string, unknown>>({
element: InnerElement,
elementProps,
}: {
element: ((props: T) => JSX.Element) & { routePath: string }
elementProps: T
}) => (
<Route
path={InnerElement.routePath}
element={<InnerElement {...elementProps} />}
/>
)
const App = (props) => (
<Routes>
<AppRoute element={MyComponent} elementProps={{ data: props.data }} />
</Routes>
)
export default App 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 🙂 . 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. |
@ryanflorence if you get asked this question a lot, perhaps you should include it in your examples where useParams is used. |
For whoever are interested to have the hook Still interested by your feedbacks here :) |
This is very informative. I wrote a hook that uses some ideas here. Thanks for having more foresight than I did. /**
* A hook to typecheck and null check useParams.
* We do this because react-router can never be sure of where we are calling useParams from.
* Even if we are sure IN THIS MOMENT, in the future, a route component could be deleted or a param name could change
* This gives us a test for that case
*/
function useTypedParams<T extends string>(parameterNames: T[]): Record<T, string> {
const params = useParams();
const typedParams: Record<string, string> = {};
parameterNames.forEach((paramName) => {
const currentParam = params[paramName];
invariant(
currentParam,
`${paramName} not found in useParams. Check the parent route to make sure nothing changed`,
);
typedParams[paramName] = currentParam;
});
return typedParams;
}
const { organizerSlug } = useTypedParams(['organizerSlug']); |
Previously we could use the |
In order to access a param in the URL, you need to give useParams a proper type argument:
|
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). |
Co-authored-by: Hiroshi Ogawa <hi.ogawa.zz@gmail.com>
Bang should be safe, see: remix-run/react-router#8200 (comment)
gives an error:
Type 'JobPageRouteParams' does not satisfy the constraint 'string'.
.The definition on
useParams
iswhich means that the generic type is supposed to be a string?? Am I missing something here?
The text was updated successfully, but these errors were encountered: