16
votes

I am getting an error trying to type the parameters for my react component. I would like to stronly type what properties will be on the props and state of a component, but when I do so using Redux, I am getting an error when I pass mapStateToProps to the connect function.

This is the component code:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

import FileExplorer from '../components/file-explorer/file-explorer';
import { ISideMenu, ISideMenuState } from '../models/interfaces/side-menu';

    class SideMenu extends Component<ISideMenu, ISideMenuState> {
        render() {
            return (
                <div>
                    {this.props.fileExplorerInfo !== null &&
                        <FileExplorer fileExplorerDirectory={this.props.fileExplorerInfo.fileExplorerDirectory}/>
                    }
                </div>
            );
        }
    }

    const mapStateToProps = (state: ISideMenuState) => {
        return {
            fileExplorerInfo: state.fileExplorer
        };
    };

    export default connect<ISideMenu, null, ISideMenuState>(mapStateToProps)(SideMenu);

So the error occurs on this line:

export default connect<ISideMenu, null, ISideMenuState>(mapStateToProps)(SideMenu);

and when I hover over the word "mapStateToProps" in that line I am seeing the error:

Argument of type '(state: ISideMenuState) => { fileExplorerInfo: FileDirectoryTree | null; }'
is not assignable to parameter of type 'MapStateToPropsParam<ISideMenu, ISideMenuState, {}>'.
  Type '(state: ISideMenuState) => { fileExplorerInfo: FileDirectoryTree | null; }' is not
assignable to type 'MapStateToProps<ISideMenu, ISideMenuState, {}>'.
    Types of parameters 'state' and 'state' are incompatible.
      Type '{}' is not
assignable to type 'ISideMenuState'.
        Property 'fileExplorer' is missing in type '{}'.

And these are the two interfaces I am using in the react component:

export interface ISideMenu {
    fileExplorerInfo: FileExplorerReducerState | null;
}

export interface ISideMenuState {
    fileExplorer: FileDirectoryTree | null;
}

Any insight into this error will be greatly appreciated!

2
this here (state: ISideMenuState) should be of your redux store state type, not the component stateDaniel Khoroshko
actually there are problems in you codeDaniel Khoroshko

2 Answers

30
votes

When using generics, you are getting the place of interfaces wrong:

When you declare your React component:

class Comp extends Component<ICompProps, ICompState>

With ICompProps and ICompState are your component's props and internal state respectively.

When you use connect:

connect<IMapStateToProps, IMapDispatchToProps, ICompProps, IReduxState>

IMapStateToProps represents what is returned by your mapStateToProps() function. IMapDispatchToProps represents what is returned by your mapDispatchToProps() function. ICompProps represents your React component props (same as above) IReduxState represents your App's Redux State

So in your particular example:

When declaring your React component:

class SideMenu extends Component<ISideMenu, {}>

Use ISideMenu for the props and {} (empty state) for the state as you don't use any state.

When using connect:

connect<ISideMenu, {}, ISideMenu, ISideMenuState>(mapStateToProps)(SideMenu);

You can use ISideMenu as both your React component props and the object returned by mapStateToProps. But in practice it might be best to create 2 separate interfaces.

In my apps, I usually can't be bothered typing the mapStateToProps return object so I simply use:

connect<{}, {}, ISideMenu, ISideMenuState>(mapStateToProps)(SideMenu);
5
votes

Hope you don't mind if I remove some antipatterns from the code above. Please check the comments I've added. Also I've added withRouter to illustrate the pattern better

import * as React from "react";
import { bindActionCreators } from "redux";
import { withRouter, RouteComponentProps } from "react-router";
import { connect } from "react-redux";
import { compose } from "recompose";

// OUR ROOT REDUX STORE STATE TYPE
import { State } from "../redux"; 

import FileExplorer from "../components/file-explorer/file-explorer";

// interfaces starting with 'I' is an antipattern and really
// rarely needed to be in a separate file

// OwnProps - that's what external code knows about out container
type OwnProps = {};

// this comes from redux
type StateProps = {
  fileExplorer: FileDirectoryTree | null;
};

// resulting props - that what container expects to have
type Props = OwnProps & StateProps & RouteComponentProps<any>;

// no need to have a class, SFC will do the same job
const SideMenu: React.SFC<Props> = props => {
  return (
    <div>
      {this.props.fileExplorerInfo !== null && (
        <FileExplorer
          fileExplorerDirectory={
            this.props.fileExplorerInfo.fileExplorerDirectory
          }
        />
      )}
    </div>
  );
};

// compose (from recompose lib) because usually we have more than 1 hoc
// say let's add withRouter just for fun



export default compose<Props, OwnProps>(

  withRouter,

  // it's important to read the typings:
  // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react-redux/index.d.ts
  connect<StateProps, {}, {}, State>(s => ({
    fileExplorerInfo: s.fileExplorer
  })),

)(SideMenu);