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?
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>
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.
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);
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);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With