REPO OWNER MESSAGE
This doc should help: https://github.com/reach/router/blob/master/website/src/markdown/pages/typescript.md
Original issue
Hi!
I'm getting this error when using Reach Router with TypeScript:

I know that this is a TypeScript-specific issue and not necessarily a bug but I'm probably not the only having this problem and getting some documentation for a workaround would be great. :)
You can solve it by specifying the types of the props of whatever page component you're using and adding a path: string; to them. That does not seem like a good idea though because you won't be actually using that prop and that will cause other errors with linters and the like.
I'm sure there are other solution but at this time I don't really know of any as I'm not a TypeScript expert. All I know that this error is due to path not being a typed prop of the page you're using.
I made a minimal test case in CodeSandbox that can reproduce it: https://codesandbox.io/s/pmkw4wr9oj (you will have to download it and open index.tsx in VS Code to see it)
I appreciate any help on this issue! Thanks!
I just came across the same issue and got around it as follows:
Install typings (npm install @types/reach__router or yarn add @types/reach__router)
Use a props interface which extends RouteComponentProps:
import { RouteComponentProps } from '@reach/router';
interface ISignupPageProps extends RouteComponentProps {
someProp: string;
}
class SignupPage extends React.Component<ISignupPageProps> {
// ...
}
or if the component doesn't have any props, use RouteComponentProps directly:
class SignupPage extends React.Component<RouteComponentProps> {
// ...
}
The type declaration includes path and some other potential props as optional, so it doesn't cause any errors if they're omitted. For reference, here's the type declaration (part of the above typings on DefinitelyTyped)
export type RouteComponentProps<TParams = {}> = Partial<TParams> & {
path?: string;
default?: boolean;
location?: WindowLocation;
navigate?: NavigateFn;
uri?: string;
};
As @maximilianschmitt suggested, it'd be great to have some guidance on this in the reach router docs.
Hey @mattwilson1024, this is great, thank you! It seems like a very sensible solution.
But actually I now have a new problem. :D It's a different one, so I created a new issue: #147
As mentioned one possible workaround is to add to all page components RouteComponentProps which seems annoying. Another, maybe nicer, solution is to create wrapper component that does this:
import React from "react";
import { Router, RouteComponentProps } from "@reach/router";
import Index from "./pages/index.jsx";
import Profile from "./pages/profile.jsx";
const App = () => (
<Router>
<RouterPage path="/" pageComponent={<Index />} />
<RouterPage path="/profile" pageComponent={<Profile />} />
</Router>
);
export default App;
const RouterPage = (
props: { pageComponent: JSX.Element } & RouteComponentProps
) => props.pageComponent;
It works when I do it exactly as @Hurtak wrote, but not when I use children as the prop instead of pageComponent:
const RouterPage = (
props: { children: JSX.Element } & RouteComponentProps
) => props.children;
...
<Router>
{routes.map(({path, component: Page}, idx) => (
<RouterPage path={path}><Home/></RouterPage>
))}
</Router>
I get:
Invariant Violation:
: Children of must have a pathordefaultprop, or be a<Redirect>. None found on element type `class Home extends react__WEBPACK_IMPORTED_MODULE_1___default.a.Component {
...
Why is that? I'd rather use children.
Hey, I found a better solution
import React, { FunctionComponent } from "react";
import { RouteComponentProps } from "@reach/router";
type Props = { component: FunctionComponent } & RouteComponentProps;
const Route: FunctionComponent<Props> = ({ component: Component, ...rest }) => (
<Component {...rest} />
);
export default Route;
<Router>
<Route component={Home} path="/" />
<Route component={NotFound} default={true} />
</Router>
And this works perfectly :)
FYI @ellipticaldoor I think you want React.ComponentType instead of FunctionComponent to allow classes too.
How does this solve the issue of params, since they aren't a property of the RouteComponentProps?
e.g. if I have <QuestionsPage path="/questions/:question_id"/>
my types:
interface IComponentProps extends IDispatchProps, RouteComponentProps {}
the IComponentProps is to ensure type safety on props added with my redux mapDispatchToProps()
and in my QuestionsPage component (which has the RouteComponentProps)
componentDidMount() {
console.log(this.props.question_id)
}
Typescript complains that question_id is not on the propTypes.
Property 'question_id' does not exist on type 'Readonly<{ children?: ReactNode; }> & Readonly<IComponentProps>'.
Of course I can add it to there, but it breaks other code, and I thought the point was not to have to extend?
Here is working solution with typed router parameters
const App = () => (
<Router>
<RouterPage path="/" pageComponent={Index} />
<RouterPage
path="/article/:slug"
pageComponent={(props: RouteComponentProps<{ slug: string }>) => (
<Article slug={props.slug} />
)}
/>
<RouterPage default pageComponent={NotFound} />
</Router>
);
const RouterPage = ({
pageComponent,
...routerProps
}: {
pageComponent: (routerProps: RouteComponentProps) => JSX.Element;
} & RouteComponentProps) => {
return <Layout>{pageComponent(routerProps)}</Layout>;
};
Thanks, I took the <{slug: string}> example and simply added it to my custom props type instead.
type TDispatchProps = {
readonly fetchCurrentQuestion: (questionId:string) => TQuestionAsyncAction
}
type TComponentProps = TDispatchProps & RouteComponentProps<{question_id: string}>
However, adding MobX store props, revisits the issue, complaining that my questionStore is required on my own props but optional with the router.
type TOwnProps = { questionStore: IQuestionStore }
type TRouterProps = RouteComponentProps<{ question_id: string | undefined } >
type TComponentProps = TOwnProps & TRouterProps;
TS2741: Property 'questionStore' is missing in type '{ path: string; }' but required in type 'Readonly<TComponentProps>'.
Of course I could set my questionStore to optional, but then I would have to manage instances where the store is injected to handle for undefined, which sort of defeats the purpose of it all. I've been trying to follow other threads on this, and there appears to be no real solution. As I mentioned in another post on this issue, Id rather change the router than weaken my types.
Sometimes TS seems hardly worth the bother
Sometimes TS seems hardly worth the bother
Understatement of the century. I'm just trying to render a simple page and, looking at this thread, it's mind bogglingly complicated to just render something simple. Who ever decided this was a great idea...
I'm just trying to render a simple functional component like this:
const Home = () => <img src={logo} className="App-logo" alt="logo" />;
Leading to errors such as:
TypeScript error: Type '{ path: string; }' is not assignable to type 'IntrinsicAttributes'.
Property 'path' does not exist on type 'IntrinsicAttributes'. TS2322
So path is not assignable to the type IntrinsicAttributes. Where do those come from, then? I haven't defined them. And it sounds to me like the TS definitions for a Routing library should probably include the "path" attribute. I'm new to TS so I have no clue how to contribute to that, yet.
So instead my code is multiplied by 11 lines into this dramatic creation:
interface IRouteComponentProps extends RouteComponentProps {
path: string
}
class Home extends Component<IRouteComponentProps> {
render() {
return (
<img src={logo} className="App-logo" alt="logo" />
);
}
}
(And now we see why TypeScript is the biggest waste of time you could possibly apply to your project. Among many other reasons.)
Can Reach-Router using TypeScript even render a simple component like this?
const Home = () => <img src={logo} className="App-logo" alt="logo" />;
Or do I really need to start billing clients per line of code written? Because then maybe TS starts being useful 馃槈
@marcelhageman,
I don't love the @reach/router experience in TypeScript either, but it's not nearly as bad as you're making it out to be.
You just need:
import { RouteComponentProps } from "@reach/router";
const Home = (_: RouteComponentProps) => <img src={logo} className="App-logo" alt="logo" />;
If you have some props that you're actually using, then you just do this:
import { RouteComponentProps } from "@reach/router";
interface Props {
alt: string;
}
const Home = ({ foo }: Props & RouteComponentProps) =>
<img src={logo} className="App-logo" alt={alt} />;
P.S. Also, check out the React+TypeScript Cheatsheets for solutions to many of these types of challenges.
@devuxer You are a saviour and a saint! That link has found a new home in my bookmarks. Cheers!
Wouldn't this be solvable by bundling a declaration merge for path on all react base component types and definitions?
I could easily be wrong here, but the idea seems to be that once you're using reach, any react component is now eligible to be routed...?
@atrauzzi,
I may be misunderstanding, but how would that work for functional components, which don't inherit from a base component type?
The .d.ts for React still defines a signature for component types (functional and class based). Going off of that, I'm pretty sure that so long as it's a type in a module (AKA: everything 馃槈), you can augment it.
Wouldn't this be solvable by bundling a declaration merge for
pathon all react base component types and definitions?
@atrauzzi,
@antmdvs addressed such an approach >> https://github.com/reach/router/issues/11#issuecomment-393999063 and codesandbox here
@devuxer works even with functional components.
My concern though is every React component now has that path attribute, whether I intend to use it for routing or not.
const Home = (_: RouteComponentProps) => <img src={logo} className="App-logo" alt="logo" />;You saved me a great deal of hair @devuxer, but some how the
underscoreis making my eslint scream big time aboutno-unused-vars.
edit Alternatively, this helps bypass the warning.
const Home: React.FC<RouteComponentProps> = () => <img src={logo} className="App-logo" alt="logo" />;
My concern though is every React component now has that path attribute, whether I intend to use it for routing or not.
@Sowed - This is going to sound super broad, but technically, when you're in a project that uses reach, that's exactly what it's doing. It's making every component eligible to be routed.
Doesn't mean you'll always take advantage of that. But the types are being augmented _only_ in the scope of your project. In another project that doesn't use reach, that hint won't be there.
@Sowed,
const Home = (_: RouteComponentProps) => <img src={logo} className="App-logo" alt="logo" />;You saved me a great deal of hair @devuxer, but some how the
underscoreis making my eslint scream big time aboutno-unused-vars.
editAlternatively, this helps bypass the warning.const Home: React.FC<RouteComponentProps> = () => <img src={logo} className="App-logo" alt="logo" />;
Interesting, with TSLint, prefixing a parameter with an underscore or using only an underscore silences that warning.
I follow @Hurtak solution and it's working. However ,how could we parse data from the URL( from the RouterPage to the pageComponent)
import React from "react";
import { Router, RouteComponentProps } from "@reach/router";
import Index from "./pages/index.jsx";
import Profile from "./pages/profile.jsx";
const App = () => (
<Router>
<RouterPage path="/" pageComponent={<Index />} />
<RouterPage path="/profile" pageComponent={<Profile />} />
</Router>
);
export default App;
const RouterPage = (
props: { pageComponent: JSX.Element } & RouteComponentProps
) => props.pageComponent;
@mustafa-alfar how about this https://github.com/reach/router/issues/141#issuecomment-458992749 ? url data should be in RouteComponentProps
@Hurtak , Thank you. I have a small question,
I try this and it works so,
However when I'm trying to destruct the id in the Home component, typescript throw an error told me props id dose not exit on type string, so it does not work except when I put it to any,
is there any suggestion to solve this issue ?
import React, { FC } from "react";
type Props = {
component: FC;
} & RouteComponentProps;
const Route: FC<Props> = ({ component: Component, ...rest }) => (
<Component {...rest} />
);
const Home: FC = ({ id }: string ): JSX.Element => (
<div>
<header>
<nav>
<Link to="/">App</Link>
<Link to="/home/1">Home</Link>
</nav>
</header>
<h1>{id}</h1>
</div>
);
ReactDOM.render(
<Router>
<Route component={Home} path="/home/:id" />
</Router>,
document.getElementById("root") as HTMLElement
);
@mustafa-alfar What you want to do is:
// setting type on the destructured property
const Home: FC = ({ id: string }): JSX.Element => (
...
or
// setting type on the argument(props)
interface HomeProps {
id: string
}
const Home: FC = ({ id }: HomeProps): JSX.Element => (
...
you were setting the type of the argument(props) to the Home fn. as string which is not what you want because id doesn't exist on the string type.
Here's an easy way to handle this with a parsed URL:
Routed component (HelloWorld.tsx):
import React, { FunctionComponent } from 'react';
import { RouteComponentProps } from '@reach/router';
// component props (you can use any type here)
interface HelloWorldProps {
name: string;
}
const HelloWorld: FunctionComponent<RouteComponentProps<HelloWorldProps>> = ({ name }) => {
// for some reason "name" here is possibly undefined
if (name) {
return <div>Hello {name}!</div>
}
return null;
}
export default HelloWorld;
Router:
import React from 'react';
import { Router } from '@reach/router';
import HelloWorld from './HelloWorld';
const App = () => (
<div>
<Router>
<HelloWorld path="/hello/:name" />
</Router>
</div>
);
This results in going to i.e. /hello/world properly displaying the name from the URL, and the route component happily compiles.
RouterPage.tsx can nest components
import React, { FC } from 'react';
import { RouteComponentProps } from "@reach/router";
interface ExtendProps extends React.PropsWithChildren<any> {
pageComponent: FC
}
const RouterPage = ({children, ...props}: ExtendProps & RouteComponentProps): React.ReactElement => {
const {pageComponent, ...others} = props;
return (
<props.pageComponent {...others}>
{children}
</props.pageComponent>
)
};
export default RouterPage;
app.tsx
<Router>
<RouterPage path="back" pageComponent={Back}>
<RouterPage path="dashboard" pageComponent={Dashboard}/>
<RouterPage path="memberManage" pageComponent={Member}/>
<RouterPage path="sys" pageComponent={Sys}>
<RouterPage path="dictManage" pageComponent={DictManage}/>
<RouterPage path="roleManage" pageComponent={RoleManage}/>
<RouterPage path="orgManage" pageComponent={OrgManage}/>
<RouterPage path="userManage" pageComponent={UserManage}/>
</RouterPage>
</RouterPage>
</Router>
back.tsx
```
import * as React from "react";
export default (props: React.PropsWithChildren
return (
{props.children}
)
}
````
Merged this, I don't use TS (obviously because look at this API 馃檭) so I can't judge the quality of it, hope it helps! (Website will be updated with this page also with the next release)
https://github.com/reach/router/blob/master/website/src/markdown/pages/typescript.md
As we iterate this project and React Router to the same hook-based API, we are taking special care to not make things so difficult on typescript users.
Strangely, if I use react-redux's connect method to wrap the component I don't get this error if I ensure that mapToStateProp method accepts ownProp as 2nd parameter - even though when I am not using ownProp to get view's props from store.
@smilealdway can you show an example of doing what you just described?
I see some comments in this thread blaming typescript for this difficulty, but I feel like this is just Typescript revealing how odd this API really is. path isn't really a prop in the sense that props are intended to be used in React, but is instead black magic that takes advantage of the prop API because of how visually appealing it is. I get that there is precedence with the key prop, but that API isn't very react-y itself in the first place tbh.
Iterating on https://github.com/reach/router/issues/141#issuecomment-458992749 with React component generics yields something fairly nice:
<Router>
<Route<{ id: string }>
path="/users/:id"
component={({ id }) =>
<UserPage id={id!} />
}
/>
</Router>
Where <Route /> is defined as:
import { RouteComponentProps } from "@reach/router";
import * as React from "react";
type Props<T extends Partial<Record<string, string>>> = RouteComponentProps<T> & {
component: (props: RouteComponentProps<T>) => React.ReactNode;
};
function Route<Params = {}>({ component, ...props }: Props<Params>) {
return <>{component(props as RouteComponentProps<Params>)}</>;
}
export default Route;
I had a look at reach briefly just now and am going back to looking at react-router 5 because of this "magic" that, while looking rather neat, causes trouble for TypeScript and leaves me wondering what's going on under the hood to make it work and under what circumstances it will break. That all said, it's great to see exploration of these sorts of things :)
"The Future of React Router and @reach/router"
"React Router v6 Preview"
They are just playing in 'touch and run'. I refuse to understand. 馃憥
Most helpful comment
I just came across the same issue and got around it as follows:
Install typings (
npm install @types/reach__routeroryarn add @types/reach__router)Use a props interface which extends
RouteComponentProps:or if the component doesn't have any props, use
RouteComponentPropsdirectly:The type declaration includes
pathand some other potential props as optional, so it doesn't cause any errors if they're omitted. For reference, here's the type declaration (part of the above typings on DefinitelyTyped)As @maximilianschmitt suggested, it'd be great to have some guidance on this in the reach router docs.