typescript: props provided by redux connect are an intersection that doesn’t allow access to actions

I have a container that I want to connect to the redux store using redux connect function. The connect function typings seems to be able to determine the resulting props, but If I try to access the inferred props on the wrapped component I get typescript errors telling me that not the mapped actions nor the stats state are present on the intersection type connect generates:

Property 'actions' does not exist on type 'PropsWithChildren<Matching<{ stats: { getSessionsPending: boolean; getSessionsError: null; }; } & { actions: { setupApp: () => (dispatch: any, getState: any) => Promise<void>; getSessions: () => GetSessionThunk; dismissGetSessionsError: () => { ...; }; }; }, Matching<...> | (ClassAttributes<...> & Matching<...>)>>'.
  Property 'actions' does not exist on type 'Matching<{ stats: { getSessionsPending: boolean; getSessionsError: null; }; } & { actions: { setupApp: () => (dispatch: any, getState: any) => Promise<void>; getSessions: () => GetSessionThunk; dismissGetSessionsError: () => { ...; }; }; }, Matching<...>> & { ...; }

This is a small reproduction example with a link to a typescript playground:

import React, { useEffect } from 'react';
import { connect } from 'react-redux';
const setupApp = () => ({type: 'setup-ok'})
const Dashboard = () => <div>Hello</div>

function mapStateToProps(state:{stats: any}) {
  return {
    stats: state.stats,
  };
}

const mapDispatchToProps = {
  actions: { setupApp },
};

const connector = connect(mapStateToProps, mapDispatchToProps);

const DefaultPage = connector(props => {
  useEffect(() => {
    const { setupApp, getSessions } = props.actions;
    setupApp().then(getSessions);
  }, []);
  const { sessions } = props.stats;
  return <Dashboard sessions={sessions} />;
});

export default DefaultPage

Answer

Your use of mapDispatch here is actually incorrect and broken.

mapDispatch can either be an object containing action creators, or a function that receives dispatch as an argument. What you have there is neither of those – it’s an object that has a nested object containing action creators, and that will not be handled correctly.

So, you either need to switch to doing const mapDispatch = {setupApp} and drop the actions nesting, or you need to write mapDispatch as a function and handle setting up the setupApp prop yourself.

You’re right that we do recommend using the ConnectedProps<T> approach if you’re using connect with TypeScript. However, we recommend using the React-Redux hooks API as the default today instead of connect, and one of the reasons is that it’s way easier to use that with TS.

All that could be simplified down to:

const DefaultPage = () => {
  const stats= useSelector( (state: RootState) => state.stats);
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(setupApp());
  }, [dispatch])
}

Leave a Reply

Your email address will not be published. Required fields are marked *