Definitelytyped: [@types/react-redux] action property type broken with @types/react-redux 6.0.0 + Redux 4 + React Redux 5.0.20

Created on 18 May 2018  路  14Comments  路  Source: DefinitelyTyped/DefinitelyTyped

Version:
Redux 4 + React Redux 5.0.20 + typesafe-actions 2.0.2+ types/react-redux 6.0.0

For example:

todoAction:

const load = createAction("[Todo] Load Todo Item");

Container:

export interface Props {
  load: () => void;
}

const TodoContainer = ({load}: Props) => {
  return (
    <div>
      <button onClick={load}>load item</button>
    </div>
  );
};

export default  connect(null, {
  load: todoAction.load
})(TodoContainer);

Error:

[ts]
Argument of type '({ load }: Props) => Element' is not assignable to parameter of type 'ComponentType<Shared<{ items: Todo[];
 loading: boolean; } & { load: () => { type: "[Todo] Load To...'.
  Type '({ load }: Props) => Element' is not assignable to type 'StatelessComponent<Shared<{ items: Todo[]; loading: boolean; } & { load: () => { type: "[Todo] Lo...'.
    Types of parameters '__0' and 'props' are incompatible.
      Type 'Shared<{ items: Todo[]; loading: boolean; } & { load: () => { type: "[Todo] Load Todo Item"; }; }...' is not assignable to type 'Props'.
        Property 'load' is optional in type 'Shared<{ items: Todo[]; loading: boolean; } & { load: () => { type: "[Todo] Load Todo Item"; }; }...' but required in type 'Props'.

It works without any issue with types/react-redux 5.0.20, but when I upgrade to 6 it shows this error.
I think it is something related to the type Shared

Most helpful comment

I got a similar problem with @types/react-redux 6.0.0 when working on a HOC. The problem consisted if I used the generic type for base Component. I reverted to @types/react-redux 5.0.20 and it worked fine.

interface UnwrappedProps {
    records: Record[]
}

interface WithRecordsProps {
    dataset: string
}

export const withRecords = <P extends UnwrappedProps>(Component: React.ComponentType<P>) => {
    class ComponentWithRecords extends React.Component<P & WithRecordsProps, {}> {
        public render() {
            return (
                <Component records={this.props.records} {...this.props} />
            )
        }
    }

    const getDatasetSelector = (state: StateStruct, props: P & WithRecordsProps) =>
        getDataset(state.datasets, props.dataset)
    const getRecordsSelector = (state: StateStruct, props: P & WithRecordsProps) =>
        state.records

    const makeGetRecordsSelector = () => createSelector(
        [getDatasetSelector, getRecordsSelector],
        (dataset, records) => dataset.records.map((record) => getRecord(records, record)),
    )

    const makeMapStateToProps = () => {
        return (state: StateStruct, props: P & WithRecordsProps) => {
            return {
                records: makeGetRecordsSelector()(state, props),
            }
        }
    }

    return connect<UnwrappedProps>(makeMapStateToProps)(ComponentWithRecords)
}

Got error:

Argument of type 'typeof ComponentWithRecords' is not assignable to parameter of type 'ComponentType<Shared<UnwrappedProps & DispatchProp<AnyAction>, P &
WithRecordsProps>>'.
  Type 'typeof ComponentWithRecords' is not assignable to type 'StatelessComponent<Shared<UnwrappedProps & DispatchProp<AnyAction>, P & WithRecordsProps>>'.
    Type 'typeof ComponentWithRecords' provides no match for the signature '(props: Shared<UnwrappedProps & DispatchProp<AnyAction>, P & WithRecordsProps> & { children?: ReactNode; }, context?: any): ReactElement<any> | null'.

All 14 comments

I got a similar problem with @types/react-redux 6.0.0 when working on a HOC. The problem consisted if I used the generic type for base Component. I reverted to @types/react-redux 5.0.20 and it worked fine.

interface UnwrappedProps {
    records: Record[]
}

interface WithRecordsProps {
    dataset: string
}

export const withRecords = <P extends UnwrappedProps>(Component: React.ComponentType<P>) => {
    class ComponentWithRecords extends React.Component<P & WithRecordsProps, {}> {
        public render() {
            return (
                <Component records={this.props.records} {...this.props} />
            )
        }
    }

    const getDatasetSelector = (state: StateStruct, props: P & WithRecordsProps) =>
        getDataset(state.datasets, props.dataset)
    const getRecordsSelector = (state: StateStruct, props: P & WithRecordsProps) =>
        state.records

    const makeGetRecordsSelector = () => createSelector(
        [getDatasetSelector, getRecordsSelector],
        (dataset, records) => dataset.records.map((record) => getRecord(records, record)),
    )

    const makeMapStateToProps = () => {
        return (state: StateStruct, props: P & WithRecordsProps) => {
            return {
                records: makeGetRecordsSelector()(state, props),
            }
        }
    }

    return connect<UnwrappedProps>(makeMapStateToProps)(ComponentWithRecords)
}

Got error:

Argument of type 'typeof ComponentWithRecords' is not assignable to parameter of type 'ComponentType<Shared<UnwrappedProps & DispatchProp<AnyAction>, P &
WithRecordsProps>>'.
  Type 'typeof ComponentWithRecords' is not assignable to type 'StatelessComponent<Shared<UnwrappedProps & DispatchProp<AnyAction>, P & WithRecordsProps>>'.
    Type 'typeof ComponentWithRecords' provides no match for the signature '(props: Shared<UnwrappedProps & DispatchProp<AnyAction>, P & WithRecordsProps> & { children?: ReactNode; }, context?: any): ReactElement<any> | null'.

I have no idea why they have v6. I think someone from them need to give the reason.

Same with me

src/stateful-navigator.tsx:81:35 - error TS2345: Argument of type '({ navigation, dispatch }: INavigatorProps) => Element' is not assignable to parameter of type 'ComponentType<Shared<{ navigation: NavigationState; } & DispatchProp<AnyAction>, INavigatorProps>>'.
  Type '({ navigation, dispatch }: INavigatorProps) => Element' is not assignable to type 'StatelessComponent<Shared<{ navigation: NavigationState; } & DispatchProp<AnyAction>, INavigatorP...'.
    Types of parameters '__0' and 'props' are incompatible.
      Type 'Shared<{ navigation: NavigationState; } & DispatchProp<AnyAction>, INavigatorProps> & { children?...' is not assignable to type 'INavigatorProps'.
        Types of property 'navigation' are incompatible.
          Type 'NavigationState | undefined' is not assignable to type 'NavigationState'.
            Type 'undefined' is not assignable to type 'NavigationState'.

81   return connect(mapStateToProps)(statefulNavigator);

This was working before without any issues.

Related to https://github.com/straw-hat-team/react-navigation-redux-helpers/blob/db275431abb7f3c3d3fa2ece91793c38d337fc89/src/stateful-navigator.tsx#L82

cc: @Kallikrein @ryym @sheetalkamat

Hi,
I'm sorry some features are provoking bugs, can you please help me and provide a light repo reproducing your issue ?
If you can't I will investigate anyway, only later.

@changLiuUNSW
@yordis
I've bootstrapped this : https://github.com/Kallikrein/debug-react-redux-typings

Can you add something to break this so I can help you fix ?

@Kallikrein
Here you are:
https://github.com/changLiuUNSW/React-Scaffold

Check TodoContainer.tsx

@changLiuUNSW I just tried.

I think Shared<>is working just fine, it may be inferring better than it was before and highlighting what was previously false negatives.

It seems your typings are not matching :

interface Props {
  items: Todo[];
  loading: boolean;
  load: () => void;
}

And your load is in fact:

interface DispatchProps {
  load: () => {
    type: TodoActionTypes.Load
  }
}

If think the error lies in

type MapDispatchToProps<TDispatchProps, TOwnProps> =
    MapDispatchToPropsFunction<TDispatchProps, TOwnProps> | TDispatchProps;

Where when mapDispatchToProps is an object (it's my first time using it), it incorrectly infers injected props as the object itself.

Instead, it should do a transform, something along the lines of:

type ObjectDispatch<TDispatchProps> = Record<keyof TDispatchProps, () => void>;

type MapDispatchToProps<TDispatchProps, TOwnProps> =
  | MapDispatchToPropsFunction<TDispatchProps, TOwnProps>
  | ObjectDispatch<TDispatchProps>;

I tried this fix by locally editing typings in your example and it's working fine.
Before I submit this fix and check it's not inducing any kind of regression, I would like your opinion on the return of these object mapDispatchToProps properties : once wrapped, do we always have a void return, or maybe something else ? (like a promise)

@suppayami your issue is missing some parts, can you dump me a file without imported typings or a repo reproducing your issue ?

I faked some typings and the following :

return connect(makeMapStateToProps)(ComponentWithRecords);

Went flawlessly. Do you really need to provide the generics to connect ? It is inferring perfectly with HOF mapStateToProps in my small mockup...

@yordis I have a feeling your various troubles may be totally unrelated, maybe it would be better to open separate issues with reproducible examples.
Don't forget to tag me, I have trouble following the overwhelming flow of issue on this repo and only subscribed to tagged notifications...

@Kallikrein Thanks for you reply.
For now, I have done following workaround for TodoContainer to eliminate the error:

interface DispatchProps {
  load: () => void;
}

interface PropsState {
  items: Todo[];
  loading: boolean;
}

const TodoContainer = ({ loading, load, items }: DispatchProps & PropsState) => {
  return (
    <div>
      <Button primary={true} onClick={load}>
        load item
      </Button>
      {loading && <p>loading...</p>}
      {!loading && <TodoList items={items} />}
    </div>
  );
};

const mapStateToProps = (state: RootState) => {
  return {
    items: getTodos(state),
    loading: getTodoLoading(state)
  };
};

export default connect<PropsState, DispatchProps>(mapStateToProps, {
  load: todoActions.load
})(TodoContainer);

I am not 100% why above workaround is working.

once wrapped, do we always have a void return, or maybe something else ? (like a promise)

Return promise is quite normal scenario especially for Redux-Thunk

It works because you are casting your connect. It can be dangerous so I would advise against it.

The best quick fix I would advise you is to replace your object mapStateToProps with a classic function

interface DispatchProps {
  load: () => void;
}
const mapDispatchToProps: MapDispatchToPropsFunction<DispatchProps, any> = dispatch => ({
  load: () => dispatch(todoActions.load())
})
export default connect(mapStateToProps, mapDispatchToProps)(TodoContainer)

You are just doing explicitely what the shortcut react-redux object sugar does for you.

This lib is a real pain, it is highly polymorphic argument number, argument types, etc, it's a horrible design "脿 la js"...

@Kallikrein Thanks for your reply.
I have tried, this also works:

interface DispatchProps {
  load: () => void;
}

interface PropsState {
  items: Todo[];
  loading: boolean;
}

const TodoContainer = ({ loading, load, items }: DispatchProps & PropsState) => {
  return (
    <div>
      <Button primary={true} onClick={load}>
        load item
      </Button>
      {loading && <p>loading...</p>}
      {!loading && <TodoList items={items} />}
    </div>
  );
};

const mapStateToProps = (state: RootState) => {
  return {
    items: getTodos(state),
    loading: getTodoLoading(state)
  };
};

export default connect(mapStateToProps, {
  load: todoActions.load
})(TodoContainer);

@changLiuUNSW One last advice
Type your lambda components as React.StatelessComponent, it provides children in case you need it.

const TodoContainer: React.StatelessComponent<DispatchProps & StateProps> = ({ loading, load, items }) => (
  <div>
    <Button primary={true} onClick={load}> load item </Button>
    {loading ? <p>loading...</p> : <TodoList items={items} />}
  </div>
);

@Kallikrein

I have done future investigation, I believe you are right. It infers better than before and highlight what was previously false negatives.

Because I have tested, my load is actually not void but actually return action object:

  load: () => {
    type: TodoActionTypes.Load
  }
// console.log (load());
// { "type": "[TODO] Todo Load" }

I have done the fix to use mapDispatchToProps and does not return action object:

const mapDispatchToProps = (dispatch: Dispatch<RootAction>) => {
  return {
    // DON'T, because this still return action object
    // load: () => dispatch(searchActions.search(input)); 
    load: () => {
      dispatch(searchActions.search(input));
    }
  };
};
Was this page helpful?
0 / 5 - 0 ratings