Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript: Why doesn't Material-UI "withStyles()" work with an explicit constructor?

Tags:

Edit:

Note that the incorrect constructor was generated by IntelliJ IDEA. This has now been fixed, see: https://youtrack.jetbrains.com/issue/WEB-35178


I am looking at using Material-UI in my app, but I'm having some trouble with the withStyles styling solution in combination with TypeScript.

I'm trying to make my own little wrapper component, based off the Popper documentation.

The problem is, if I define a constructor explicitly the way I'm used to ((B) in the code below), then this line:

export default withStyles(styles)(HelpComponent);

Give this error:

ERROR in [at-loader] ./src/main/ts/screen/keyword/HelpComponent.tsx:150:35 
    TS2345: Argument of type 'typeof HelpComponent' is not assignable to parameter of type 'ComponentType<never>'.
  Type 'typeof HelpComponent' is not assignable to type 'StatelessComponent<never>'.
    Type 'typeof HelpComponent' provides no match for the signature '(props: never, context?: any): ReactElement<any> | null'.

The only way I've been able to make this work is to omit the explicit constructor and define state as a field ((A) in the code below).

Is there a way to declare a constructor the normal way when I use withStyles?

I actually don't mind setting the state directly like this, it's a bit less boilerplate. Am I giving up anything by initialising the state outside the constructor this way?

Ultimately, I just don't understand that TypeScript error message - can anyone explain what it's trying to say?

import {
  createStyles,
  Paper,
  Popper,
  Theme,
  WithStyles,
  withStyles
} from '@material-ui/core';
import * as React from 'react';
import {ReactNode, SyntheticEvent} from 'react';
import {EventUtil} from "appUtil/EventUtil";
import {WarningSvg} from "component/svg-icon/WarningSvg";
let log = require("log4javascript").getLogger("HelpComponent");

export interface HelpComponentProps extends WithStyles<typeof styles> {
  children:ReactNode;
}

export interface HelpComponentState {
  open:boolean;
  arrowRef?: HTMLElement;
}

class HelpComponent extends React.Component<HelpComponentProps, HelpComponentState> {
  helpRef!: HTMLElement;

  // (A)
  state = {open: false, arrowRef: undefined};

  // (B)
  // constructor(props: HelpComponentProps, context: HelpComponentState){
  //   super(props, context);
  //   this.state = {open: false, arrowRef: undefined};
  // }

  handleClick = (event:SyntheticEvent<any>) => {
    EventUtil.stopClick(event);
    this.setState({open: !this.state.open,});
  };

  handleArrowRef = (node:HTMLElement) => {
    this.setState({
      arrowRef: node,
    });
  };

  render(){
    const {classes} = this.props;
    return <span ref={(ref)=>{if(ref) this.helpRef = ref}}>
      <WarningSvg onClick={this.handleClick}/>
      <Popper id={"help-popper"} className={classes.popper} transition
        open={this.state.open} anchorEl={this.helpRef}
        modifiers={{arrow:{enabled:true, element: this.state.arrowRef}}}
      >
        <span className={classes.arrow} ref={this.handleArrowRef}/>
        <Paper className={classes.paper}>{this.props.children}</Paper>
      </Popper>
    </span>;
  }

}

const styles = (theme: Theme) => createStyles({
  root: {
    flexGrow: 1,
  },
  scrollContainer: {
    height: 400,
    overflow: 'auto',
    marginBottom: theme.spacing.unit * 3,
  },
  scroll: {
    position: 'relative',
    width: '230%',
    backgroundColor: theme.palette.background.paper,
    height: '230%',
  },
  legend: {
    marginTop: theme.spacing.unit * 2,
    maxWidth: 300,
  },
  paper: {
    maxWidth: 400,
    overflow: 'auto',
  },
  select: {
    width: 200,
  },
  popper: {
    zIndex: 1,
    '&[x-placement*="bottom"] $arrow': {
      top: 0,
      left: 0,
      marginTop: '-0.9em',
      width: '3em',
      height: '1em',
      '&::before': {
        borderWidth: '0 1em 1em 1em',
        borderColor: `transparent transparent ${theme.palette.common.white} transparent`,
      },
    },
    '&[x-placement*="top"] $arrow': {
      bottom: 0,
      left: 0,
      marginBottom: '-0.9em',
      width: '3em',
      height: '1em',
      '&::before': {
        borderWidth: '1em 1em 0 1em',
        borderColor: `${theme.palette.common.white} transparent transparent transparent`,
      },
    },
    '&[x-placement*="right"] $arrow': {
      left: 0,
      marginLeft: '-0.9em',
      height: '3em',
      width: '1em',
      '&::before': {
        borderWidth: '1em 1em 1em 0',
        borderColor: `transparent ${theme.palette.common.white} transparent transparent`,
      },
    },
    '&[x-placement*="left"] $arrow': {
      right: 0,
      marginRight: '-0.9em',
      height: '3em',
      width: '1em',
      '&::before': {
        borderWidth: '1em 0 1em 1em',
        borderColor: `transparent transparent transparent ${theme.palette.common.white}`,
      },
    },
  },
  arrow: {
    position: 'absolute',
    fontSize: 7,
    width: '3em',
    height: '3em',
    '&::before': {
      content: '""',
      margin: 'auto',
      display: 'block',
      width: 0,
      height: 0,
      borderStyle: 'solid',
    },
  },
});

export default withStyles(styles)(HelpComponent);

Versions:

  • "typescript": "3.0.3"
  • "@types/react": "16.4.14"
  • "@types/react-dom": "16.0.7"
  • "@material-ui/core": "3.1.1"
  • "webpack": "4.19.1"
like image 956
Shorn Avatar asked Oct 01 '18 06:10

Shorn


1 Answers

If you look at the definition of React.ComponentType:

type ComponentType<P = {}> = ComponentClass<P> | StatelessComponent<P>;

The alternative you are trying to use is ComponentClass, which is defined as:

interface ComponentClass<P = {}, S = ComponentState> extends StaticLifecycle<P, S> {
    new (props: P, context?: any): Component<P, S>;
    // ...
}

Notice that the context parameter is optional (? mark). The problem is that the context parameter of your constructor is not optional, so HelpComponent is not compatible with callers that expect to be able to omit the context argument. If you make the parameter optional, the error should go away.

When TypeScript reports the error that typeof HelpComponent is not assignable to the union type React.ComponentType, it semi-arbitrarily picks one member of the union type to report a detailed error. Unfortunately, it didn't pick the one you intended, so the error message isn't very useful.

I actually don't mind setting the state directly like this, it's a bit less boilerplate. Am I giving up anything by initialising the state outside the constructor this way?

Yes, by overriding the state property, you may be unintentionally changing its type to be something different from the state type argument that you passed to the React.Component base class.

like image 93
Matt McCutchen Avatar answered Oct 21 '22 11:10

Matt McCutchen