Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to properly type a Redux connect call?

I am trying to use a Redux state store in combination with TypeScript. I am trying to use the official Typings of Redux and want to make the entire call on the connect method (which connects the mapStatetoProps and mapDispatchToProps with a component) type safe.

I usually see approaches where the methods mapStatetoProps and mapDispatchToProps are just custom typed and return a partial of the component props, such as the following:

function mapStateToProps(state: IStateStore, ownProps: Partial<IComponentProps>)
  : Partial<IProjectEditorProps> {}
function mapDispatchToProps (dispatch: Dispatch, ownProps: Partial<IComponentProps>)
  : Partial<IProjectEditorProps> {}

This is typed and works, but not really safe because it is possible to instantiate a component which misses props, as the usage of the Partial interface allows incomplete definitions. However, the Partial interface is required here because you may want to define some props in mapStateToProps and some in mapDispatchToProps, and not all in one function. So this is why I want to avoid this style.

What I am currently trying to use is directly embedding the functions in the connect call and typing the connect call with the generic typing supplied by redux:

connect<IComponentProps, any, any, IStateStore>(
  (state, ownProps) => ({
    /* some props supplied by redux state */
  }),
  dispatch => ({
    /* some more props supplied by dispatch calls */
  })
)(Component);

However, this also throws the error that the embedded mapStatetoProps and mapDispatchToProps calls to not define all Props each as both only require a subset of them, but together defining all Props.

How can I properly type the connect call so that the mapStatetoProps and mapDispatchToProps calls are really type safe and typing checks if the combined values defined by both methods supply all required props without one of the methods required to define all props at once? Is this somehow possible with my approach?

like image 679
Lukas Bach Avatar asked Jan 12 '19 00:01

Lukas Bach


Video Answer


3 Answers

Option 1: Split IComponentProps

The simplest way to do this is probably just defining separate interfaces for your "state derived props", your "own props" and your "dispatch props" and then use an intersection type to join them together for the IComponentProps

import * as React from 'react';
import { connect, Dispatch } from 'react-redux'
import { IStateStore } from '@src/reducers';


interface IComponentOwnProps {
  foo: string;
}

interface IComponentStoreProps {
  bar: string;
}

interface IComponentDispatchProps {
  fooAction: () => void;
}

type IComponentProps = IComponentOwnProps & IComponentStoreProps & IComponentDispatchProps

class IComponent extends React.Component<IComponentProps, never> {
  public render() {
    return (
      <div>
        foo: {this.props.foo}
        bar: {this.props.bar}
        <button onClick={this.props.fooAction}>Do Foo</button>
      </div>
    );
  }
}

export default connect<IComponentStoreProps, IComponentDispatchProps, IComponentOwnProps, IStateStore>(
  (state, ownProps): IComponentStoreProps => {
    return {
      bar: state.bar + ownProps.foo
    };
  },
  (dispatch: Dispatch<IStateStore>): IComponentDispatchProps => (
    {
      fooAction: () => dispatch({type:'FOO_ACTION'})
    }
  )
)(IComponent);

We can set the connect function generic parameters like so: <TStateProps, TDispatchProps, TOwnProps, State>

Option 2: Let your functions define your Props interface

Another option I've seen in the wild is to leveraging the ReturnType mapped type to allow your mapX2Props functions to actually define what they contribute to IComponentProps.

type IComponentProps = IComponentOwnProps & IComponentStoreProps & IComponentDispatchProps;

interface IComponentOwnProps {
  foo: string;
}

type IComponentStoreProps = ReturnType<typeof mapStateToProps>;
type IComponentDispatchProps = ReturnType<typeof mapDispatchToProps>;

class IComponent extends React.Component<IComponentProps, never> {
  //...
}


function mapStateToProps(state: IStateStore, ownProps: IComponentOwnProps) {
  return {
    bar: state.bar + ownProps.foo,
  };
}

function mapDispatchToProps(dispatch: Dispatch<IStateStore>) {
  return {
    fooAction: () => dispatch({ type: 'FOO_ACTION' })
  };
}

export default connect<IComponentStoreProps, IComponentDispatchProps, IComponentOwnProps, IStateStore>(
  mapStateToProps,
  mapDispatchToProps
)(IComponent);

The big advantage here is that it reduces a little bit of the boiler plate and makes it so you only have one place to update when you add a new mapped prop.

I've always steered away from ReturnType, simplify because it feels backwards to let your implementation define your programming interface "contracts" (IMO). It becomes almost too easy to change your IComponentProps in a way you didn't intend.

However, since everything here is pretty self contained, it's probably an acceptable use case.

like image 70
NSjonas Avatar answered Oct 14 '22 07:10

NSjonas


One solution is to split your component properties into state-, dispatch- and maybe own-properties:

import React from "react";
import { connect } from "react-redux";

import { deleteItem } from "./action";
import { getItemById } from "./selectors";

interface StateProps {
  title: string;
}

interface DispatchProps {
  onDelete: () => any;
}

interface OwnProps {
  id: string;
}

export type SampleItemProps = StateProps & DispatchProps & OwnProps;

export const SampleItem: React.SFC<SampleItemProps> = props => (
  <div>
    <div>{props.title}</div>
    <button onClick={props.onDelete}>Delete</button>
  </div>
);

// You can either use an explicit mapStateToProps...
const mapStateToProps = (state: RootState, ownProps: OwnProps) : StateProps => ({
  title: getItemById(state, ownProps.id)
});

// Ommitted mapDispatchToProps...

// ... and infer the types from connects arguments ...
export default connect(mapStateToProps, mapDispatchToProps)(SampleItem);

// ... or explicitly type connect and "inline" map*To*.
export default connect<StateProps, DispatchProps, OwnProps, RootState>(
  (state, ownProps) => ({
    title: getItemById(state, ownProps.id)
  }),
  (dispatch, ownProps) => ({
    onDelete: () => dispatch(deleteItem(ownProps.id))
  })
)(SampleItem);
like image 2
sn42 Avatar answered Oct 14 '22 06:10

sn42


Really like @NSjonas's splitting approach, but I'd borrow something from his second approach as well to have a balance among practicality, not letting the implementation completely define your interface and not being extremely verbose in typing your dispatch actions;

import * as React from 'react';
import { connect, Dispatch } from 'react-redux'
import { IStateStore } from '@src/reducers';
import { fooAction } from '@src/actions';


interface IComponentOwnProps {
  foo: string;
}

interface IComponentStoreProps {
  bar: string;
}

interface IComponentDispatchProps {
  doFoo: (...args: Parameters<typeof fooAction>) => void;
}

type IComponentProps = IComponentOwnProps & IComponentStoreProps & IComponentDispatchProps

class IComponent extends React.Component<IComponentProps, never> {
  public render() {
    return (
      <div>
        foo: {this.props.foo}
        bar: {this.props.bar}
        <button onClick={this.props.doFoo}>Do Foo</button>
      </div>
    );
  }
}

export default connect<IComponentStoreProps, IComponentDispatchProps, IComponentOwnProps, IStateStore>(
  (state, ownProps): IComponentStoreProps => {
    return {
      bar: state.bar + ownProps.foo
    };
  },
  {
      doFoo: fooAction
  }
)(IComponent);

like image 1
Karuhanga Avatar answered Oct 14 '22 06:10

Karuhanga