'Preserve type argument in Redux connected component

I have a simple component that takes a type argument on props. When used, it infers the prop type and contextually types a callback param. However, when I wrap the component in connect(), it loses the type argument inference and falls back to unknown. Example:

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

type MyCompProps<T> = { 
    value: T; 
    onChange(value: T): void;
}

class MyComp<T> extends Component<MyCompProps<T>> { }

<MyComp value={ 25 } onChange={ val => {/* val will be contextually typed to `number` ✅ */} }/>

const stateToProps = (state: any) => ({ });

const dispatchToProps = { };

const MyContainer = connect(stateToProps, dispatchToProps)(MyComp);

<MyContainer value={ 25 } onChange={ val => {/* val is now `unknown` ❌ */} }/>

Is there any way to make MyContainer behave like MyComp? Either by passing in explicit type args to connect() or by writing an assertion on the resulting type?



Solution 1:[1]

After a lot failed attempts to make connect() behave as I wanted, I found the simplest solution was to just wrap the connected component in another component that re-exposed the type argument and renders the connected component:

const ConnectedMyComp = connect(stateToProps, dispatchToProps)(MyComp);

class MyContainer<T> extends Component<MyCompProps<T>> {
    render() {
        return <ConnectedMyComp { ...this.props }/>;
    }
}

<MyContainer value={ 25 } onChange={ val => {/* val is `number` ? */} }/>

Full example that also deals with the whole dance around own props, state props, and dispatch props: Playground

Solution 2:[2]

There's two different answers here.

The first is that we show a technique for automatically inferring "the type of all props from Redux" in our React-Redux docs "Usage with TS" usage guide page, by using the ConnectedProps<T> type and separating the connect call into two pieces:

import { connect, ConnectedProps } from 'react-redux'

interface RootState {
  isOn: boolean
}

const mapState = (state: RootState) => ({
  isOn: state.isOn,
})

const mapDispatch = {
  toggleOn: () => ({ type: 'TOGGLE_IS_ON' }),
}

const connector = connect(mapState, mapDispatch)

// The inferred type will look like:
// {isOn: boolean, toggleOn: () => void}
type PropsFromRedux = ConnectedProps<typeof connector>

That may help here, although tbh I really don't know if connect will preserve a generic type like that.

That said, the better answer is:

use function components and the hooks API instead of class components and connect!

The hooks API is simpler to use overall, and works much better with TypeScript. In addition, because you're no longer dealing with Higher-Order Components, there's no interference with whatever generic props types you might be using.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Aaron Beall
Solution 2 markerikson