Definitelytyped: @types/react-redux: Inferred wrong type in `connect` when component props is optional

Created on 24 Sep 2017  路  30Comments  路  Source: DefinitelyTyped/DefinitelyTyped

  • [x] I tried using the @types/xxxx package and had problems.
  • [x] I tried using the latest stable version of tsc. https://www.npmjs.com/package/typescript
  • [x] I have a question that is inappropriate for StackOverflow. (Please ask any appropriate questions there).
  • [x] [Mention](https://github.com/blog/821-mention-somebody-they-re-notified) the authors (see Definitions by: in index.d.ts) so they can respond.

    • Authors: @....

Mentioning @blakeembrey, @andy-ms, @alecmerdler 馃槈

Code:

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

import { RootState } from "@src/state/state";

interface Props {
    normal: string;
    optional?: number;
}
class TestComponent extends Component<Props> {
    render() {
        return <h1>Hello</h1>;
    };
}
function mapStateToProps(_state: RootState) {
    return { 
        normal: "test",
        optional: 5,
    };
}
const Connected = connect(mapStateToProps)(TestComponent);

Error:

[ts]
Argument of type 'typeof TestComponent' is not assignable to parameter of type 'ComponentType<{ normal: string; optional: number | undefined; } & DispatchProp<any>>'.
  Type 'typeof TestComponent' is not assignable to type 'StatelessComponent<{ normal: string; optional: number | undefined; } & DispatchProp<any>>'.
    Type 'typeof TestComponent' provides no match for the signature '(props: { normal: string; optional: number | undefined; } & DispatchProp<any> & { children?: ReactNode; }, context?: any): ReactElement<any> | null'.

The problem is when the optional prop is marked as optional - the TestComponent part of connect(mapStateToProps)(TestComponent); is reported red with above error code.

For unknown reasons the inferred type is StatelessComponent but TestComponent is a class.
When the prop is marked as optional: number|undefined all is ok, so it's a workaround for now.

Most helpful comment

I hit this. You can fix easily enough with a cast.

function mapStateToProps(_state: RootState) {
    return { 
        normal: "test",
        optional: 5,
    } as Partial<Props>;
}

At least it works for me.

All 30 comments

The workaround doesn't work when we use 3rd party HOC, like redux-form, which has some props optional, like initialValues?: Partial<FormData>;:

Code to reproduce:

interface State {
    stateString: string;
}

interface CompProps {
    propString: string;
}
interface FormData {
    name: string;
    age: number;
}
class TestComponent extends Component<CompProps& InjectedFormProps<FormData, CompProps>> {}

export const Form = reduxForm<FormData, CompProps>({
    form: "test",
    destroyOnUnmount: true,
})(TestComponent);

export const Connected = connect(
    (state: State) => ({
        propString: state.stateString,
        initialValues: { name: "", age: 23 },
    }),
)(Form);

I got this not meaningful error:

Argument of type 'DecoratedComponentClass<FormData, CompProps & Partial<ConfigProps<FormData, CompProps>>>' is not assignable to parameter of type 'ComponentType<{ propString: string; initialValues: { name: string; age: number; }; } & DispatchPr...'.
  Type 'DecoratedComponentClass<FormData, CompProps & Partial<ConfigProps<FormData, CompProps>>>' is not assignable to type 'StatelessComponent<{ propString: string; initialValues: { name: string; age: number; }; } & Dispa...'.
    Type 'DecoratedComponentClass<FormData, CompProps & Partial<ConfigProps<FormData, CompProps>>>' provides no match for the signature '(props: { propString: string; initialValues: { name: string; age: number; }; } & DispatchProp<any> & { children?: ReactNode; }, context?: any): ReactElement<any> | null'.

In connect I can specify only non optional and not undefined props, like "form". Even marking as required and with undefined union doesn't work in this case.

The pick props from redux-form config works ok, if I doesn't provide form name in redux-form, I had to to this in connect or in JSX <Form form="test" />.

Ping authors after 2 weeks 馃槈
@tkqubo @thasner @kenzierocks @clayne11 @tansongyang @NicholasBoll @mDibyo

Looks like we should update the Diff and Omit types to the new ones?
https://github.com/Microsoft/TypeScript/issues/12215#issuecomment-319495340

I believe this may have been fixed by https://github.com/DefinitelyTyped/DefinitelyTyped/pull/21730. I needed to make additional changes in my project to account for https://github.com/DefinitelyTyped/DefinitelyTyped/pull/21400 but after upgrading to v5.0.14 everything seems happy again.

@rafeememon
Unfortunately not 馃槥 I've installed 5.0.14 and the error is still here:
image
image
The problem is that connect types perform the type check from inverse side, so optional type a?: A is not assignable to type a: A|undefined.

And because of that, when your props are a: A, you can assign maybe undefined value to the a field and have no error at all, like photo: 1 !== 1 ? state.photos[0] : undefined 馃檧

Mentioning all authors for any help: @tkqubo, @thasner, @kenzierocks, @clayne11, @tansongyang, @nicholasboll, @mdibyo, @pdeva

There still seems to be issues with optional props as described by @19majkel94 one comment above. I am seeing similar behaviour and was able to reproduce this in a simple test case:

namespace MapStateOptionalProp {

    interface Props {
        bar?: number,
        test: boolean
    }

    const TestComponent: React.StatelessComponent<Props> = ({bar = 1}) => null

    const mapStateToProps = (_: any) => ({
        bar: 1,
        test: false
    })

    const Test = connect(
        mapStateToProps
    )(TestComponent)

    const verify = <Test />
}

This fails with:

[ts] Argument of type 'StatelessComponent<Props>' is not assignable to parameter of type 'ComponentType<{ bar: number; test: boolean; } & DispatchProp<any>>'.

on types/react-redux 5.0.14 and typescript 2.6.2. Any suggestions what might be the issue?

Any suggestions what might be the issue?

Yes, the problem is with HOC - they behave like function that accept an argument (a component). So connect infer the props type from mapStateToProps function and the compiler check if the component props match the connect props. So it failed because bar is number so Props with optional bar is not assignable.

The solution is to have interfaces for mapStateToProps props type to disable the inferring of not optional type.

Having the same issue here :/
Fun fact: it works normally with functional components, but not with class components.

I hit this. You can fix easily enough with a cast.

function mapStateToProps(_state: RootState) {
    return { 
        normal: "test",
        optional: 5,
    } as Partial<Props>;
}

At least it works for me.

@DanHarman Partial<Props> - Props coming from where?

@nwinger My suggestion was if you had code as per the original in the first post.

@19majkel94 Could you try this? (Adapted the example from the initial post in this issue)

Only the last line has changed.

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

import { RootState } from "@src/state/state";

interface Props {
    normal: string;
    optional?: number;
}
class TestComponent extends Component<Props> {
    render() {
        return <h1>Hello</h1>;
    };
}
function mapStateToProps(_state: RootState) {
    return { 
        normal: "test",
        optional: 5,
    };
}
const Connected = connect<Props>(mapStateToProps)(TestComponent);

What about mapDispatchToProps and OwnProps? I had to create 3 interfaces and the union type to merge them together in Props. I know it works, the issue is about inferencing from shape of connect parameters and it can't be done for now as it's how TS works with HOC 馃槙

@19majkel94 Just read the rest of the discussion above thoroughly. Sorry for bringing up something that has already been discussed.

And I understand your concern with having to define the extra interfaces/types. Quite often, more than half of my component file is spent on that boilerplate.

@DanHarman

Casting to Partial<Props> can be error-prone. If you forget to provide a required prop, typescript won't report such error.

It works fine if we cast it to a stateless component.

connect(mapStateToProps, mapDispatch)(MyComponent as any as React.SFC<Props>)

Or we can use conditional types to create an util for converting to SFC. It's a better solution if we don't have Props definition.

type ToSFC<T> = T extends React.ComponentClass<infer Props> ? React.SFC<Props> : T;

function toSFC<T>(component: T) {
  return component as ToSFC<T>;
}

connect(mapStateToProps, mapDispatch)(toSFC(MyComponent))

I had this issue. The problem was the return type of my mapStateToProps functions didn't have the same optionals the original Props type did. I fixed it by using Pick in the following way, pulled from my code based on the Typescript React tutorial:

export function mapStateToProps({ enthusiasmLevel, languageName }: StoreState) {
  let props = {
    enthusiasmLevel,
    name: languageName
  }
  return props as Pick<Props, keyof typeof props> // this is the key line
}

Here's my full example:

interface Props {
  name: string             // this comes from redux state
  enthusiasmLevel?: number // this comes from redux state
  onIncrement?: () => any  // this comes from redux dispatch
  onDecrement?: () => any  // this comes from redux dispatch
  greeting: string         // this is passed through the container component
}

class Hello extends React.Component<Props> {
  render() {
    return ( /* snipped for brevity */ )
  }
}

function mapStateToProps({ enthusiasmLevel, languageName }: MyState) {
  let props = {
    enthusiasmLevel,
    name: languageName
  }
  return props as Pick<Props, keyof typeof props>
}

function mapDispatchToProps(dispatch: Dispatch<actions.EnthusiasmAction>) {
  let props = {
    onIncrement: () => dispatch(actions.incrementEnthusiasm()),
    onDecrement: () => dispatch(actions.decrementEnthusiasm())
  }
  return props as Pick<Props, keyof typeof props>
}

export default connect(mapStateToProps, mapDispatchToProps)(Hello)

After sometime of search, this is the best solution doc of TypeScript + React:

https://github.com/piotrwitek/react-redux-typescript-guide

Most of scenarios with solution will be found here. And for my use, I use Material UI, I'm glad to share my demos below.

Example: SFC

import * as React from 'react'
import { formatSex, formatDocState, formatGoal, formatPriority } from '@app/state/ducks/crm/doc/types'
import TableRow from '@material-ui/core/TableRow'
import TableCell from '@material-ui/core/TableCell'
import Hidden from '@material-ui/core/Hidden'
import { RouteComponentProps, withRouter } from 'react-router-dom'
import { Theme, withStyles, WithStyles } from '@material-ui/core/styles'
import config from '@app/../config'

const V = config.VERSION

const styles = (theme: Theme) => ({
  row: {
    '&:nth-of-type(odd)': {
      backgroundColor: theme.palette.background.default,
    },
  },
})

export interface DocListItemViewProps extends RouteComponentProps<{}> {
  id: number
  name: string
  sex: number
  priority: number
  dealPossibility: number
  mobile: string
  docState: number
  goal: number
}

type PropsWithStyles = DocListItemViewProps & WithStyles<'row'>

const DocListItem: React.SFC<PropsWithStyles> = (props) => {
  const { id, name, priority, dealPossibility, mobile, sex, docState, goal, classes } = props
  return (
    <TableRow
      onClick={() => {
        props.history.push(`/${V}/crm/doc/updateDoc/${id}`)
      }}
      className={classes.row}
    >
      <Hidden mdDown={true}>
        <TableCell>{id}</TableCell>
      </Hidden>
      <TableCell>{name}</TableCell>
      <Hidden xsDown={true}>
        <TableCell>{formatSex(sex)}</TableCell>
      </Hidden>
      <TableCell>{formatPriority(priority)}</TableCell>
      <Hidden xsDown={true}>
        <TableCell>{formatDocState(docState)}</TableCell>
      </Hidden>
      <Hidden smDown={true}>
        <TableCell>{dealPossibility}</TableCell>
      </Hidden>
      <Hidden lgDown={true}>
        <TableCell>{formatGoal(goal)}%</TableCell>
      </Hidden>
      <TableCell>{mobile}</TableCell>
    </TableRow>
  )
}
export default withRouter(withStyles(styles)(DocListItem))

Example: Component

import * as React from 'react'
import CRMLayout from '@app/views/layout/CRMLayout'
import { withStyles, WithStyles } from '@material-ui/core/styles'
import { hot } from 'react-hot-loader'
import { connect } from 'react-redux'
import { RootState } from 'src/app/state/rootReducer'
import * as SharedActions from '@app/state/ducks/shared/actions'
import { BOTTOM_NAVIGATION_ITEM_TYPE } from '@app/state/ducks/shared/types'
import Avatar from '@material-ui/core/Avatar'
import Paper from '@material-ui/core/Paper'
import * as classnames from 'classnames'
import DataSummaryItem from '@app/views/components/common/DataSummaryItem'
import NameSummaryItem from '@app/views/components/common/NameSummaryItem'
import ScheduleList from '@app/views/components/common/ScheduleList'
import ActivityList from '@app/views/components/common/ActivityList'

const decorator = withStyles(({ spacing }) => ({
  container: {
  },
  block: {
    marginBottom: spacing.unit,
    padding: spacing.unit * 2,
  },
  summary: {
    display: 'flex',
    flexDirection: 'row' as 'row',
  },
  schedules: {
  },
  activities: {
  },
  avatar: {
    width: 60,
    height: 60,
  },
}))

interface CRMIndexProps {
  switchBottomNavigation: (item: BOTTOM_NAVIGATION_ITEM_TYPE) => any
}

interface CRMIndexStates {
}

type PropsWithStyles = CRMIndexProps & WithStyles<'container' | 'block' | 'summary' | 'schedules' | 'activities' | 'avatar'>

const CRMIndex = decorator(
  class extends React.Component<PropsWithStyles, CRMIndexStates> {

    componentDidMount() {
      this.props.switchBottomNavigation(BOTTOM_NAVIGATION_ITEM_TYPE.CRM_INDEX)
    }

    render() {
      const { classes } = this.props
      return (
        <CRMLayout>
          <div className={classes.container}>
            <Paper square={true} className={classnames(classes.summary, classes.block)}>
              <Avatar
                src="https://cdn.bolifestudio.com/public/uploads/1_2_avatar_1522051846699?x-oss-process=image/resize,w_128"
                className={classes.avatar}
              />
              <NameSummaryItem name="Bosn" title="Test" />
              <DataSummaryItem label="aaa" value="ccc" />
              <DataSummaryItem label="bbb" value="ddd" />
            </Paper>
            <Paper square={true} className={classnames(classes.schedules, classes.block)}>
              <ScheduleList />
            </Paper>
            <Paper square={true} className={classnames(classes.activities, classes.block)}>
              <ActivityList />
            </Paper>
          </div>
        </CRMLayout>
      )
    }

  }
)

const mapStateToProps = (_state: RootState) => {
  return {
  }
}

const mapDispatchToProps = ({
  switchBottomNavigation: SharedActions.switchBottomNavigation,
})

export default connect(mapStateToProps, mapDispatchToProps)(hot(module)(CRMIndex))

@Bosn, if I'm reading your examples correctly, I believe your second example would not compile if switchBottomNavigation was optional in interface CRMIndexProps, which is the issue in this thread. @BetterCallSky's React.SFC example solves that issue, but then causes errors with pass through props (ones that are not mapped from state or dispatch). I'll have to work with it a bit longer to be 100% sure, but I believe that using Pick in the mapping functions (as in my example above) cleanly solves both issues while maintaining type inference in connect and minimizing code duplication.

@sargunv I think this issue had been fixed, I use the newest version packages, and try to rebuild the error, but it compiles successfully. My code:

import * as React from 'react'
import { connect } from 'react-redux'
import { RootState } from 'src/app/state/rootReducer'

export interface DocListViewProps {
  optional?: number
}
export interface DocListViewStates {
}


class SalesAnalyze extends React.Component<DocListViewProps, DocListViewStates> {
  render() {
    const { optional } = this.props
    return (
      <div>
        sales charts page{optional}
      </div>
    )
  }
}

const mapStateToProps = (_state: RootState) => {
  return {
  }
}

const mapDispatchToProps = ({
})

export default connect(mapStateToProps, mapDispatchToProps)(SalesAnalyze)

@Bosn You need to populate the optional prop in mapStatesToProps to reproduce the error.

@sargunv The example given by the issue author can be compiled successfully via TypeScript 2.9.0 (Using VSCode Insider, at right/bottom corner, choose 2.9.0-insiders.20180510

For older versions, this can be fixed by just adding function returning type of mapStateToProps as Props, this is the full example.

import * as React from 'react'
import { connect } from 'react-redux'

interface RootState {
}

interface Props {
    normal: string
    optional?: number
}
class TestComponent extends React.Component<Props> {
    render() {
        return <h1>Hello</h1>
    }
}
function mapStateToProps(_state: RootState): Props {  // here, I add the type to match, or use new derived interface.
    return {
        normal: 'test',
        optional: 5,
    }
}
const Connected = connect(mapStateToProps)(TestComponent)

export default Connected

For convenience, also can use Partial<Props> as the returning type of mapStateToProps. For strictly use, I think it'd be better to define by yourself to match the interface below.

interface MapProps {
    normal: string
    optional?: number
}

function mapStateToProps(_state: RootState): MapProps { 
    return {
        normal: 'test',
        optional: 5,
    }
}

@Bosn That's exactly what the Pick solution is doing, except without duplicating the prop definitions.

These two code blocks are essentially equivalent, but the second avoids defining the normal and optional props twice.

interface Props {
  normal: string
  optional?: number
  other: string
}

interface MapProps {
    normal: string
    optional?: number
}

function mapStateToProps(_state: RootState): MapProps { 
  return {
    normal: 'test',
    optional: 5,
  }
}
interface Props {
  normal: string
  optional?: number
  other: string
}

function mapStateToProps(_state: RootState) { 
  let props = {
    normal: 'test',
    optional: 5,
  }
  return props as Pick<Props, keyof typeof props>
}

@sargunv Your solution is shorter, but can't be right. You can see my example, when property normal is REQUIRED, for your method, all properties will be OPTIONAL.

import * as React from 'react'
import { connect } from 'react-redux'

interface RootState {
}

interface Props {
    normal: string
    optional?: number
    other: string
}
class TestComponent extends React.Component<Props> {
    render() {
        return <h1>Hello</h1>
    }
}
function mapStateToProps(_state: RootState) {
    const props =  {
        other: 'haha',  // property normal required !!! But no compile error~
    }
    return props as Pick<Props, keyof typeof props>
}
const Connected = connect(mapStateToProps)(TestComponent)

export default Connected

Posting this here for feedback to this issue...

I hate declaring all Redux props as optional, so I opt to create two interfaces; Props & ReduxProps. That way when I implement the component, I don't get any extraneous properties showing (the optional redux-connect props). Problem is, when I use connect() I get a ts error that says

Argument of type 'typeof TestComponent' is not assignable to parameter of type 'ComponentType<never>'.
  Type 'typeof TestComponent' is not assignable to type 'ComponentClass<never, any>'.
    Types of property 'defaultProps' are incompatible.
      Type 'Partial<Props>' is not assignable to type 'never'.

In order to fix that issue and get sane code completion, I have resorted to casting the components. Here's what I've done, written as the TestComponent:

import * as React from 'react'
import { connect } from 'react-redux'

interface Props {
  normal: string
  optional?: number
  other: string
}

interface ReduxProps {
  isAuthenticated: boolean;
  username: string;
}
class TestComponent extends React.Component<Props & ReduxProps> {
  constructor(props: Props & ReduxProps) {
    super(props);
  }
  render() {
      return <h1>Hello{this.props.isAuthenticated ? ` ${this.props.username}` : ''}</h1>
  }
}
const mapState = state => ({
  isAuthenticated: state.auth.isAuthenticated,
  username: state.auth.username,
});

// Casting to prevent TS from throwing error output as above
const Connected = connect(
  mapStateToProps
)(
 TestComponent as React.ComponentClass<Props & ReduxProps> // cast the connect-input component with all props
) as React.ComponentClass<Props> // cast the final component just with Props

export default Connected

Now code completion will only show normal, other, optional?

I hate having to do the extra casting so if you have suggestions on why this is necessary please fire away.

Any clue how to fix this at the source?
In index.d.ts I'll have a look around the Omit of InferableComponentEnhancerWithProps...

Any hint is welcome :')

This is still an issue, a year later. Is there anyone that can identify and fix this issue?

@DylanVann I saw that you commited recently on this project, would you mind to take a look on this, or redirect to someone who can? Thx

I'd try using the hooks API instead of using connect. Typing HOCs is too painful.

@DylanVann I far as I can see, hooks api works for function components, we are using class components.

For all participants in this thread, if you use yarn (I'm not sure about npm), please refer the typings version on resolutions node of package.json.

These are my versions:

"resolutions": {
        "@types/react": "16.9.11",
        "@types/prop-types": "15.7.3",
        "@types/react-dom": "16.9.4",
        "@types/react-redux": "7.1.5"
    },

After this change some incompatible type errors are gone.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

csharpner picture csharpner  路  3Comments

JWT
svipas picture svipas  路  3Comments

fasatrix picture fasatrix  路  3Comments

Loghorn picture Loghorn  路  3Comments

victor-guoyu picture victor-guoyu  路  3Comments