Skip to content

Type errors in connected HOC in [email protected] #100

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
jakub-astrahit opened this issue Oct 28, 2018 · 10 comments
Closed

Type errors in connected HOC in [email protected] #100

jakub-astrahit opened this issue Oct 28, 2018 · 10 comments

Comments

@jakub-astrahit
Copy link

jakub-astrahit commented Oct 28, 2018

Hi, I have an HOC based on this example: https://github.com/piotrwitek/react-redux-typescript-guide/blob/master/playground/src/hoc/with-state.tsx

I am trying to use connect() on this HOC but the typings don't work - I have to use as any at the end when I return. How can I fix this code so that it works without as any please?

import * as React from "react";
import { connect } from "react-redux";
import { compose, Dispatch } from "redux";
import { setTheme } from "src/modules/config/configActions";
import { LocalStorage, Theme } from "src/modules/config/configModel";
import { State } from "src/store";
import { setItem } from "src/utils/localStorage";
import { Subtract } from "utility-types";

interface InjectedProps {
  theme: Theme;
  toggleTheme: (event: React.MouseEvent<HTMLElement>) => void;
}

const mapStateToProps = (state: State) => ({
  theme: state.config.theme
});

const mapDispatchToProps = (dispatch: Dispatch) => ({
  changeTheme: (theme: Theme) => {
    dispatch(setTheme(theme));
  }
});

export const withNavBar = <WrappedProps extends InjectedProps>(
  WrappedComponent: React.ComponentType<WrappedProps>
) => {
  type HocProps = Subtract<WrappedProps, InjectedProps> & {
    theme: Theme;
    changeTheme: (theme: Theme) => void;
    toggleTheme: (event: React.MouseEvent<HTMLElement>) => void;
  };

  class WithNavBar extends React.Component<HocProps, {}> {
    public static displayName = `withNavBar(${WrappedComponent.name})`;
    public static readonly WrappedComponent = WrappedComponent;

    public render() {
      const { ...restProps } = this.props as {};

      return (
        <WrappedComponent
          {...restProps}
          theme={this.props.theme}
          toggleTheme={this.toggleTheme}
        />
      );
    }

    private toggleTheme = (event: React.MouseEvent<HTMLElement>) => {
      const { theme, changeTheme } = this.props;
      let newTheme: Theme = theme;
      if (theme === Theme.dark) {
        newTheme = Theme.light;
      } else if (theme === Theme.light) {
        newTheme = Theme.dark;
      }
      changeTheme(newTheme);
      if (localStorage) {
        setItem(LocalStorage.theme, newTheme);
      }
    };
  }

  return compose(
    connect(
      mapStateToProps,
      mapDispatchToProps
    )
  )(WithNavBar as any); // <-------- how to remove "as any"?
};


If I don't use as any, then I get this typescript error:

(70,5): Argument of type 'typeof WithNavBar' is not assignable to parameter of type 'ComponentType<Matching<{ theme: Theme | null; } & { changeTheme: (theme: Theme) => void; }, HocProps>>'.
  Type 'typeof WithNavBar' is not assignable to type 'ComponentClass<Matching<{ theme: Theme | null; } & { changeTheme: (theme: Theme) => void; }, HocProps>, any>'.    Types of parameters 'props' and 'props' are incompatible.
      Type 'Matching<{ theme: Theme | null; } & { changeTheme: (theme: Theme) => void; }, HocProps>' is not assignable to type 'Readonly<HocProps>'.        Type 'P extends "theme" | "changeTheme" ? ({ theme: Theme | null; } & { changeTheme: (theme: Theme) => void; })[P] extends HocProps[P] ? HocProps[P] : ({ theme: Theme | null; } & { changeTheme: (theme: Theme) => void; })[P] : HocProps[P]' is not assignable to type 'HocProps[P]'.
          Type 'HocProps[P] | (({ theme: Theme | null; } & { changeTheme: (theme: Theme) => void; })[P] extends HocProps[P] ? HocProps[P] : ({ theme: Theme | null; } & { changeTheme: (theme: Theme) => void; })[P])' is not assignable to type 'HocProps[P]'.
            Type '({ theme: Theme | null; } & { changeTheme: (theme: Theme) => void; })[P] extends HocProps[P] ? HocProps[P] : ({ theme: Theme | null; } & { changeTheme: (theme: Theme) => void; })[P]' isnot assignable to type 'HocProps[P]'.
              Type 'HocProps[P] | ({ theme: Theme | null; } & { changeTheme: (theme: Theme) => void; })[P]' is not assignable to type 'HocProps[P]'.
                Type '({ theme: Theme | null; } & { changeTheme: (theme: Theme) => void; })[P]' is not assignable to type 'HocProps[P]'.
                  Type '{ theme: Theme | null; } & { changeTheme: (theme: Theme) => void; }' is not assignable to type 'HocProps'.
                    Type '{ theme: Theme | null; } & { changeTheme: (theme: Theme) => void; }' is not assignable to type 'Pick<WrappedProps, SetDifference<keyof WrappedProps, "theme" | "toggleTheme">>'.
                      Type 'keyof T extends "theme" | "changeTheme" ? ({ theme: Theme | null; } & { changeTheme: (theme: Theme) => void; })[keyof T] extends HocProps[keyof T] ? HocProps[keyof T] : ({ theme: Theme | null; } & { ...; })[keyof T] : HocProps[keyof T]' is not assignable to type 'HocProps[P]'.                        Type 'HocProps[keyof T] | (({ theme: Theme | null; } & { changeTheme: (theme: Theme) => void; })[keyof T] extends HocProps[keyof T] ? HocProps[keyof T] : ({ theme: Theme | null; }& { changeTheme: (theme: Theme) => void; })[keyof T])' is not assignable to type 'HocProps[P]'.
                          Type 'HocProps[keyof T]' is not assignable to type 'HocProps[P]'.
                            Type 'keyof T' is not assignable to type 'P'.
                              Type 'string | number | symbol' is not assignable to type 'P'.
                                Type 'string' is not assignable to type 'P'.
                                  Type 'HocProps[string] | HocProps[number] | HocProps[symbol]' is not assignable to type 'HocProps[P]'.
                                    Type 'WrappedProps[string]' is not assignable to type 'HocProps[P]'.
                                      Type 'WrappedProps' is not assignable to type 'HocProps'.
                                        Type 'InjectedProps' is not assignable to type 'HocProps'.
                                          Type 'InjectedProps' is not assignable to type 'Pick<WrappedProps, SetDifference<keyof WrappedProps, "theme" | "toggleTheme">>'.
                                            Type 'WrappedProps' is not assignable to type '{ theme: Theme; changeTheme: (theme: Theme) => void; toggleTheme: (event: MouseEvent<HTMLElement>) => void; }'.
                                              Type 'InjectedProps' is not assignable to type '{ theme: Theme; changeTheme: (theme: Theme) => void; toggleTheme: (event: MouseEvent<HTMLElement>) => void; }'.
                                                Property 'changeTheme' is missing in type 'InjectedProps'.

How can I fix the code above so that it works without as any please?

@piotrwitek
Copy link
Owner

piotrwitek commented Oct 31, 2018

Property 'changeTheme' is missing in type 'InjectedProps'

I don't see changeTheme declared in InjectedProps interface

@jakub-astrahit
Copy link
Author

jakub-astrahit commented Nov 18, 2018

@piotrwitek No that doesn't help. The problem is that I want to dispatch an action inside the HOC, not in the wrapped component. Let me use your example from the docs. I can't connect it to the redux store properly. I get this error:

(51,29): Argument of type 'typeof WithState' is not assignable to parameter of type 'ComponentType<Matching<DispatchProp<AnyAction>, HocProps>>'.
  Type 'typeof WithState' is not assignable to type 'ComponentClass<Matching<DispatchProp<AnyAction>, HocProps>, any>'.
    Types of parameters 'props' and 'props' are incompatible.
      Type 'Matching<DispatchProp<AnyAction>, HocProps>' is not assignable to type 'Readonly<HocProps>'.
        Type 'P extends "dispatch" ? DispatchProp<AnyAction>[P] extends HocProps[P] ? HocProps[P] : DispatchProp<AnyAction>[P] : HocProps[P]' is not assignable to type 'HocProps[P]'.
          Type 'HocProps[P] | (DispatchProp<AnyAction>[P] extends HocProps[P] ? HocProps[P] : DispatchProp<AnyAction>[P])' is not assignable to type 'HocProps[P]'.
            Type 'DispatchProp<AnyAction>[P] extends HocProps[P] ? HocProps[P] : DispatchProp<AnyAction>[P]' is not assignable to type 'HocProps[P]'.
              Type 'HocProps[P] | DispatchProp<AnyAction>[P]' is not assignable to type 'HocProps[P]'.
                Type 'DispatchProp<AnyAction>[P]' is not assignable to type 'HocProps[P]'.
                  Type 'DispatchProp<AnyAction>' is not assignable to type 'HocProps'.
                    Type 'DispatchProp<AnyAction>' is not assignable to type 'Pick<WrappedProps, SetDifference<keyof WrappedProps, "count" | "onIncrement">>'.
                      Type 'keyof T extends "dispatch" ? DispatchProp<AnyAction>[keyof T] extends HocProps[keyof T] ? HocProps[keyof T] : DispatchProp<AnyAction>[keyof T] : HocProps[keyof T]' is not assignable to type 'HocProps[P]'.
                        Type 'HocProps[keyof T] | (DispatchProp<AnyAction>[keyof T] extends HocProps[keyof T] ? HocProps[keyof T] : DispatchProp<AnyAction>[keyof T])' is not assignable to type 'HocProps[P]'.
                          Type 'HocProps[keyof T]' is not assignable to type 'HocProps[P]'.
                            Type 'keyof T' is not assignable to type 'P'.
                              Type 'string | number | symbol' is not assignable to type 'P'.
                                Type 'string' is not assignable to type 'P'.
                                  Type 'HocProps[string] | HocProps[number] | HocProps[symbol]' is not assignable to type 'HocProps[P]'.
                                    Type 'WrappedProps[string]' is not assignable to type 'HocProps[P]'.
                                      Type 'WrappedProps' is not assignable to type 'HocProps'.
                                        Type 'InjectedProps' is not assignable to type 'HocProps'.
                                          Type 'InjectedProps' is not assignable to type 'Pick<WrappedProps, SetDifference<keyof WrappedProps, "count" | "onIncrement">>'.
                                            Type 'WrappedProps' is not assignable to type '{ initialCount?: number | undefined; }'.
                                              Type 'InjectedProps' has no properties in common with type '{ initialCount?: number | undefined; }'.

And here is the code:

import * as React from "react";
import { connect } from "react-redux";
import { compose } from "redux";
import { Subtract } from "utility-types";

// These props will be subtracted from original component type
interface InjectedProps {
  count: number;
  onIncrement: () => any;
}

export const withState = <WrappedProps extends InjectedProps>(
  WrappedComponent: React.ComponentType<WrappedProps>
) => {
  // These props will be added to original component type
  type HocProps = Subtract<WrappedProps, InjectedProps> & {
    // here you can extend hoc props
    initialCount?: number;
  };
  type HocState = {
    readonly count: number;
  };

  class WithState extends React.Component<HocProps, HocState> {
    // Enhance component name for debugging and React-Dev-Tools
    public static displayName = `withState(${WrappedComponent.name})`;
    // reference to original wrapped component
    public static readonly WrappedComponent = WrappedComponent;

    public readonly state: HocState = {
      count: Number(this.props.initialCount) || 0
    };

    public handleIncrement = () => {
      this.setState({ count: this.state.count + 1 });
    };

    public render() {
      const { ...restProps } = this.props as {};
      const { count } = this.state;

      return (
        <WrappedComponent
          {...restProps}
          count={count} // injected
          onIncrement={this.handleIncrement} // injected
        />
      );
    }
  }
  return compose(connect())(WithState);
};

Any ideas please?

@piotrwitek
Copy link
Owner

piotrwitek commented Nov 19, 2018

@jakub-astrahit
Ok I know that issue, had the same recently with TS v3.1.6, it is related to some breaking changes in the TypeScript.

I can share with you some workaround that work for a moment, but I will search for a better solution within nightly build on the next branch.

Note: You have to adapt it yourself for your own use-case, unfortunately I don't have time for that. Getting rid of Subtract as I did should fix the type error.

import React from 'react';
import qs from 'query-string';
import { withRouter, RouteComponentProps } from 'react-router-dom';

const DISPLAY_NAME = 'withQueryParams';

export interface WithQueryParamsInjectedProps<T> {
    setQueryParams(params: T): void;
    queryParams: T;
}

/**
 * @description injects `queryParams` and `setQueryParams` to read and manipulate query string params
 *
 * @example
 * ```
 * import withQueryParams, { WithQueryParamsInjectedProps } from '...';
 *
 * type QueryParams = {
 *     page?: string;
 *     per_page?: string;
 *     search?: string;
 *     order_by?: string;
 * };
 *
 * export interface Props extends WithQueryParamsInjectedProps<QueryParams> {
 *    organizationId: number;
 * }
 *
 * const Component: React.SFC<Props> = ({ queryParams, setQueryParams }) => {
 *     // queryParams = { page, per_page, search, order_by };
 *     setQueryParams({
 *         ...queryParams,
 *         page: 2,
 *     })
 * }
 *
 * export default withQueryParams(Component);
 * ```
 */
export default function <WrappedComponentProps extends Partial<WithQueryParamsInjectedProps<{}>>>(
    WrappedComponent: React.ComponentType<WrappedComponentProps>,
) {
    type HocProps = WrappedComponentProps
        & RouteComponentProps;

    const Hoc: React.SFC<HocProps> = (props) => {
        const { history, location } = props;

        const setQueryParams = (params: object) => {
            history.push({
                pathname: location.pathname,
                search: '?' + qs.stringify(params),
            });
        };

        const getQueryParams = (): object => qs.parse(location.search);

        return (
            <WrappedComponent
                setQueryParams={setQueryParams}
                queryParams={getQueryParams()}
                {...props}
            />
        );
    };

    Hoc.displayName = `${DISPLAY_NAME}(${WrappedComponent.name})`;

    return withRouter(Hoc);
}

@piotrwitek piotrwitek reopened this Nov 19, 2018
@piotrwitek piotrwitek self-assigned this Nov 19, 2018
@piotrwitek piotrwitek changed the title Types error when using HOC and connect() from redux Type errors in HOC Example in [email protected] Nov 19, 2018
@jakub-astrahit
Copy link
Author

@piotrwitek I tried your workaround (I modified it to match my need), but it doesn't work. I'm trying to use connect() from redux but it gives me this error now:

Failed to compile.
Type error: Argument of type 'typeof Hoc' is not assignable to parameter of type 'ComponentType<Matching<{ openDialogs: IDialog[]; } & { toggleDialog: (dialogId: string) => void; }, HocProps>>'.
  Type 'typeof Hoc' is not assignable to type 'ComponentClass<Matching<{ openDialogs: IDialog[]; } & { toggleDialog: (dialogId: string) => void; }, HocProps>, any>'.
    Types of parameters 'props' and 'props' are incompatible.
      Type 'Matching<{ openDialogs: IDialog[]; } & { toggleDialog: (dialogId: string) => void; }, HocProps>' is not assignable to type 'Readonly<HocProps>'.
        Type 'P extends "toggleDialog" | "openDialogs" ? ({ openDialogs: IDialog[]; } & { toggleDialog: (dialogId: string) => void; })[P] extends HocProps[P] ? HocProps[P] : ({ openDialogs: IDialog[]; } & { toggleDialog: (dialogId: string) => void; })[P] : HocProps[P]' is not assignable to type 'HocProps[P]'.
          Type 'HocProps[P] | (({ openDialogs: IDialog[]; } & { toggleDialog: (dialogId: string) => void; })[P] extends HocProps[P] ? HocProps[P] : ({ openDialogs: IDialog[]; } & { toggleDialog: (dialogId: string) => void; })[P])' is not assignable to type 'HocProps[P]'.
            Type '({ openDialogs: IDialog[]; } & { toggleDialog: (dialogId: string) => void; })[P] extends HocProps[P] ? HocProps[P] : ({ openDialogs: IDialog[]; } & { toggleDialog: (dialogId: string) => void; })[P]' is not assignable to type 'HocProps[P]'.
              Type 'HocProps[P] | ({ openDialogs: IDialog[]; } & { toggleDialog: (dialogId: string) => void; })[P]' is not assignable to type 'HocProps[P]'.
                Type '({ openDialogs: IDialog[]; } & { toggleDialog: (dialogId: string) => void; })[P]' is not assignable to type 'HocProps[P]'.
                  Type '{ openDialogs: IDialog[]; } & { toggleDialog: (dialogId: string) => void; }' is not assignable to type 'HocProps'.
                    Type '{ openDialogs: IDialog[]; } & { toggleDialog: (dialogId: string) => void; }' is not assignable to type 'WrappedComponentProps'.
                      Type 'keyof T extends "toggleDialog" | "openDialogs" ? ({ openDialogs: IDialog[]; } & { toggleDialog: (dialogId: string) => void; })[keyof T] extends HocProps[keyof T] ? HocProps[keyof T] : ({ openDialogs: IDialog[]; } & { ...; })[keyof T] : HocProps[keyof T]' is not assignable to type 'HocProps[P]'.
                        Type 'HocProps[keyof T] | (({ openDialogs: IDialog[]; } & { toggleDialog: (dialogId: string) => void; })[keyof T] extends HocProps[keyof T] ? HocProps[keyof T] : ({ openDialogs: IDialog[]; } & { ...; })[keyof T])' is not assignable to type 'HocProps[P]'.
                          Type 'HocProps[keyof T]' is not assignable to type 'HocProps[P]'.
                            Type '(WithDialogProps & StateProps)[keyof T]' is not assignable to type 'HocProps[P]'.
                              Type 'WithDialogProps & StateProps' is not assignable to type 'HocProps'.
                                Type 'WithDialogProps & StateProps' is not assignable to type 'WrappedComponentProps'.
                                  Type 'keyof T' is not assignable to type 'P'.
                                    Type 'string | number | symbol' is not assignable to type 'P'.
                                      Type 'string' is not assignable to type 'P'.
                                        Type 'HocProps[string] | HocProps[number] | HocProps[symbol]' is not assignable to type 'HocProps[P]'.
                                          Type 'WrappedComponentProps[string]' is not assignable to type 'HocProps[P]'.
                                            Type 'string' is not assignable to type 'P'.  TS2345

    50 |       mapDispatchToProps
    51 |     )
  > 52 |   )(Hoc);
       |     ^
    53 | };

Here is my component code:

import React from "react";
import { connect } from "react-redux";
import { compose, Dispatch } from "redux";
import { State } from "../../../store";
import { toggleDialog } from "../dialogActions";
import { IDialog } from "../dialogModels";

export interface WithDialogProps {
  toggleDialog: (dialogId: string) => void;
}

interface StateProps {
  openDialogs: IDialog[];
}

const mapStateToProps = (state: State) => ({
  openDialogs: state.dialogs.openDialogs
});

const mapDispatchToProps = (dispatch: Dispatch) => ({
  toggleDialog: (dialogId: string) => {
    dispatch(toggleDialog(dialogId));
  }
});

export const withDialog = <
  WrappedComponentProps extends WithDialogProps & StateProps
>(
  WrappedComponent: React.ComponentType<WrappedComponentProps>
) => {
  type HocProps = WrappedComponentProps & WithDialogProps;

  class Hoc extends React.Component<HocProps, {}> {
    static displayName = `withDialog(${WrappedComponent.name})`;
    static readonly WrappedComponent = WrappedComponent;

    render() {
      return (
        <WrappedComponent
          toggleDialog={this.props.toggleDialog}
          {...this.props}
        />
      );
    }
  }

  return compose(
    connect(
      mapStateToProps,
      mapDispatchToProps
    )
  )(Hoc);
};

@IssueHuntBot
Copy link

@issuehuntfest has funded $20.00 to this issue. See it on IssueHunt

@piotrwitek piotrwitek changed the title Type errors in HOC Example in [email protected] Type errors in connected HOC in [email protected] Feb 12, 2019
@sachadvt
Copy link

@jakub-astrahit, I think that your compose is just closed too soon, try this (it worked for me):

return compose( connect( mapStateToProps, mapDispatchToProps ), Hoc);

@piotrwitek
Copy link
Owner

Hey @jakub-astrahit, I reproduced your issue in my component and it looks like the issue is in connect type-definitions, because other higher order components work just fine.

I would suggest filing an issue on definitely typed, I cannot do anything more with this here.

@IssueHuntBot
Copy link

@piotrwitek has cancelled @IssueHunt's funding for this issue.(Cancelled amount: $20.00) See it on IssueHunt

@jakub-astrahit
Copy link
Author

jakub-astrahit commented May 3, 2019

@sachadvt Your approach removes the type error (that's great!), but the component can't be used - it gives me this error when used:

JSX element type '{}' is not a constructor function for JSX elements.
  Type '{}' is missing the following properties from type 'Element': type, props, key  TS2605

@piotrwitek The only solution for me now is to do this:

 return connect(
    mapStateToProps,
    mapDispatchToProps,
  )(Hoc as any)

where Hoc is my class:
class Hoc extends React.Component<HocProps, {}> { ....

@DBosley
Copy link

DBosley commented Aug 15, 2019

I think I figured a way to get this working for my own problem. I'm also using thunk, but I don't think the pattern should differ much if you don't need it. Here's an example:

interface UserStateProps {
  user: User;
}

interface UserDispatchProps {
  setUser: (user: User) => void;
  fetchUser: () => void;
}

export type UserProps = UserStateProps & UserDispatchProps;

const mapState = <P>(state: object, ownProps: P): UserStateProps & P => {
  const user = getUser(state);
  return {
    user,
    ...ownProps
  };
};

function mapDispatch(dispatch: ThunkDispatch<{}, {}, AnyAction>): UserDispatchProps {
  return {
    setUser: (user): void => {
      dispatch(setUser(user));
    },
    fetchUser: (): void => {
      dispatch(fetchUser());
    }
  };
}

export const withUser = <P extends UserProps, C extends ComponentType<Matching<UserProps, GetProps<C>>>>(
  Component: C
): ConnectedComponentClass<C, Omit<GetProps<C>, keyof Shared<UserProps, GetProps<C>>> & Omit<P, keyof UserProps>>=> {
  const enhancer = connect<UserStateProps, UserDispatchProps, Omit<P, keyof UserProps>, {}>(
    mapState,
    mapDispatch
  );
  return enhancer(Component);
};

I created a helper type to more easily construct that complex ConnectedComponentClass

export type ConnectedComponent<
  TComponent extends ComponentType<{}>,
  TAllProps,
  TMappedProps
> = ConnectedComponentClass<
  TComponent,
  Omit<GetProps<TComponent>, keyof Shared<TMappedProps, GetProps<TComponent>>> &
    Omit<TAllProps, keyof TMappedProps>
>;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants