react-router-dom with TypeScript
Asked Answered
A

7

64

I'm trying to use react router with TypeScript. However, I have certain problems using withRouter function. On the last line, I'm getting pretty weird error:

Argument of type 'ComponentClass<{}>' is not assignable to parameter of type 'StatelessComponent<RouteComponentProps<any>> | ComponentClass<RouteComponentProps<any>>'.
  Type 'ComponentClass<{}>' is not assignable to type 'ComponentClass<RouteComponentProps<any>>'.
    Type '{}' is not assignable to type 'RouteComponentProps<any>'.
      Property 'match' is missing in type '{}’

Code looks like:

import * as React from 'react';
import { connect } from 'react-redux';
import { RouteComponentProps, withRouter } from 'react-router-dom';

interface HomeProps extends RouteComponentProps<any> {
}

interface HomeState { }

class Home extends React.Component<HomeProps, HomeState> {
  constructor(props: HomeProps) {
    super(props);
  }
  public render(): JSX.Element {
    return (<span>Home</span>);
  }
}

const connectModule = connect(
  (state) => ({
    // Map state to props
  }),
  {
    // Map dispatch to props
  })(Home);

export default withRouter(connectModule);
Alvar answered 22/5, 2017 at 16:39 Comment(0)
M
66

I use a different approach to fix this. I always separate the different properties (router, regular and dispatch), so I define the following interfaces for my component:

interface HomeRouterProps {
  title: string;   // This one is coming from the router
}

interface HomeProps extends RouteComponentProps<HomeRouterProps> {
  // Add your regular properties here
}

interface HomeDispatchProps {
  // Add your dispatcher properties here
}

You can now either create a new type that combines all properties in a single type, but I always combine the types during the component definition (I don't add the state here, but if you need one just go ahead). The component definition looks like this:

class Home extends React.Component<HomeProps & HomeDispatchProps> {
  constructor(props: HomeProps & HomeDispatchProps) {
    super(props);
  }

  public render() {
    return (<span>{this.props.match.params.title}</span>);
  }
}

Now we need to wire the component to the state via a container. It looks like this:

function mapStateToProps(state, ownProps: HomeProps): HomeProps => {
  // Map state to props (add the properties after the spread)
  return { ...ownProps };
}

function mapDispatchToProps(dispatch): HomeDispatchProps {
  // Map dispatch to props
  return {};
}

export default connect(mapStateToProps, mapDispatchToProps)(Hello);

This method allows a fully typed connection, so the component and container are fully typed and it is safe to refactor it. The only thing that isn't safe for refactoring is the parameter in the route that is mapped to the HomeRouterProps interface.

Musgrave answered 23/10, 2017 at 21:44 Comment(4)
Where's your call to withRouter?Gridiron
Upvoted. This is really the "right" way to solve the issue. If you are using TypeScript, type your components properly!Gridiron
Now, this is about a year old, but isn't HelloDispatchProps supposed to be HomeDispatchProps?Correa
In mapStateToProps, shouldn't be ownProps of type RouteComponentProps<HomeRouterProps>?Big
F
20

I think it is a typescript typings compilation issue, but I've found a workaround:

interface HomeProps extends RouteComponentProps<any>, React.Props<any> {
}
Faery answered 15/6, 2017 at 15:43 Comment(3)
I would more so label this as expected behavior. Kevin Welcher outlines why this is needed a little more here: medium.com/@kaw2k/a-letter-of-appreciation-253ecab3f7d2Phlegmatic
Declaring any on typings removes the purpose of using typings in the first place. Wrong approach.Myrilla
Yes, it is not the correct approach, that's why I classified it as workaround.Faery
P
6

It looks like you have the right usage to apply the match, history, and location props to your component. I would check in your node_modules directory to see what versions of react-router and react-router-dom you have, as well as the @types modules.

I have essentially the same code as you, and mine is working with the following versions:

{
  "@types/react-router-dom": "^4.0.4",
  "react-router-dom": "^4.1.1",
}
Precautious answered 13/6, 2017 at 2:20 Comment(0)
M
6

This is a Typescript typings issue. Since withRouter() will provide the routing props you need at runtime, you want to tell consumers that they only need to specify the other props. In this case, you could just use a plain ComponentClass:

export default withRouter(connectModule) as React.ComponentClass<{}>;

Or, if you had other props (defined in an interface called OwnProps) you wanted to pass in, you could do this:

export default withRouter(connectModule) as React.ComponentClass<OwnProps>;

There is slightly more discussion here

Mcandrew answered 23/7, 2017 at 11:6 Comment(1)
withRouter uses generics, so you can type it rather than typecasting: export default withRouter<OwnProps>(connectModule);.Amourpropre
C
4

After browsing the typescript definitions I discovered the RouteComponentProps interface so I now model my containers like so

type RouteParams = {
    teamId: string; // must be type string since route params
}

interface Props extends RouteComponentProps<RouteParams>, React.Props<RouteParams> { }

type State = {
    players: Array<Player>;
}

export class PlayersContainer extends React.Component<Props, State>{} 

now in the component class the route props can be accessed like this: let teamid = this.props.match.params.teamId;

Concise answered 22/9, 2018 at 5:2 Comment(0)
O
2

+1 for Ramon's answer. You can absolutely get full type coverage here. To add a bit - I added the call to withRouter.

interface FooProps extends RouteComponentProps<Foo> {
  foo: string;
}

const Foo = ({ history, foo }: FooProps) => <span/>;

const RoutedFoo = withRouter(Foo);

my dependencies:

"@types/react-router-dom": "^4.3.0",
"typescript": "^2.9.2",
Oleson answered 29/8, 2018 at 17:26 Comment(0)
Q
2

The way how I'm doing this:

interface IOwnProps {
  style?: CSSProperties;
}

interface INavProps {
  item?: string;
}

type IProps = IOwnProps & RouteComponentProps<INavProps>;

export default class MapScreen extends PureComponent<IProps> {
  ...
}
Quimper answered 16/5, 2019 at 15:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.