Material-ui: [Typescript] Typing a class component constructor when using withStyles

Created on 24 Oct 2017  路  29Comments  路  Source: mui-org/material-ui

I am trying the following:

const decorate = withStyles(({ palette }) => ({
  root: {
  }
}));

class MyComponent extends React.Component<WithStyles<'root'>> {

  // What should be the type of the props?
  constructor(props: WithStyles<'root'>) {
    super(props);
    // ... do things
  }

  render() {
    const cl = this.props.classes!;
    return <div className={cl.root}>hi</div>;
  }
}

export default decorate<{}>(MyComponent);

However struggling to work out the right constructor props type

Error:(67, 29) TS2345:Argument of type 'typeof MyClass' is not assignable to parameter of type 'ComponentType<WithStyles<"root">>'.
  Type 'typeof MyClass' is not assignable to type 'StatelessComponent<WithStyles<"root">>'.
    Type 'typeof MyClass' provides no match for the signature '(props: WithStyles<"root"> & { children?: ReactNode; }, context?: any): ReactElement<any> | null'.

The Typescript Guide in the docs doesn't provide an example to show how to type a constructor

  • [x] I have searched the issues of this repository and believe that this is not a duplicate.

Your Environment

| Tech | Version |
|--------------|---------|
| Material-UI | 1.0.0-beta.17 |
| React | 16.0.0 (using @types/react 16.0.18 |

typescript

Most helpful comment

Hi @pelotom, here is my use case:

// Home.tsx
import * as React from 'react';
import Hello from './Hello';

interface IHomeProps {}

export default class Home extends React.Component<IHomeProps> {
    public render() {
        return <Hello name="Bob" />;
    }
}
// hello.tsx
import * as React from 'react';
import { Grid } from 'material-ui';
import withWidth, { WithWidthProps } from 'material-ui/utils/withWidth';
import withStyles, { WithStyles } from 'material-ui/styles/withStyles';
import { compose } from 'redux';
import { Theme } from 'material-ui/styles';

const styles = (theme: Theme) => ({
    root: {
        backgroundColor: theme.palette.common.faintBlack
    }
});

interface IHelloProps {
    name?: string;
}

export class Hello extends React.Component<IHelloProps & WithWidthProps & WithStyles<'root'>> {
    public static defaultProps = {
        name: 'Alex'
    };

    public render() {
        return (
            <Grid
                className={this.props.classes.root}
                direction={this.props.width === 'sm' ? 'column' : 'row'}
            >
                <h1>Hello {this.props.name}!</h1>
            </Grid>
        );
    }
}

const enhance = compose(withWidth(), withStyles(styles));

export default enhance(Hello);

The error I have on Home.tsxis the following

Type '{ name: "Bob"; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<Component<WithWidthProps, ComponentState>> & Reado...'.
  Type '{ name: "Bob"; }' is not assignable to type 'Readonly<WithWidthProps>'.
    Property 'width' is missing in type '{ name: "Bob"; }'.

All 29 comments

Your snippet works fine for me... what version of TypeScript are you using?

Oh sorry forgot to add that to the env , 2.6-rc

Ah, it looks like this is due to TypeScript 2.6's --strictFunctionTypes combined with the fact that the actual signature of the constructor for React.Component is

constructor(props?: P, context?: any);

So you need to mark your props argument to the constructor as optional:

constructor(props?: WithStyles<'root'>) {
  super(props)
  // ... do things
}

Digression and speculation

I'm not sure why it's marked optional; afaik the React component's constructor will never be called with undefined props. Maybe it's because they want it to be optional to call

super(props)

If it's just that, and the concrete component will always be passed defined props, then it seems like the typing for React.ComponentClass is wrong; instead of

new (props?: P, context?: any): Component<P, ComponentState>;

it should be

new (props: P, context?: any): Component<P, ComponentState>;

Opened a PR for @types/react that would address this.

@pelotom Interesting, ok thanks for the diagnosis, will follow the progress of the PR too. Think you are right about the props, should always be set

Hi, I've struggled with it a lot of time. Here's the solution of using styles, themes and TS:

import * as React from 'react';
import { WithStyles, Button, withStyles } from 'material-ui';
import { StyleRules, Theme } from 'material-ui/styles';

const styleClasses = {
  button: '',
};

const styles = (theme: Theme): StyleRules<keyof typeof styleClasses> => (
  { button: {
      backgroundColor: theme.palette.primary[200]
    }
  }
);

interface MyButtonProps {
  label: string;
}

class MyButton extends React.Component< MyButtonProps & WithStyles<keyof typeof styleClasses>> {
  render () {
    const classes = this.props.classes ? this.props.classes : styleClasses;
    return (
      <Button className={classes.button}>this.props.label</Button>
    );
  }
}

export default withStyles(styles)(MyButton);

This solution exports the ComponentClass with unnecessary "classes" proprty. Type checking works well - if you change styleClasses, the compiller will throw an error.

The main thing is to use & operator in types, not extending WithStyles or using it directly. If you see what does withStyles function you'll notice that it takes the component

The second thing is that you use styleClasses as default initializator in render() function .Without it you will need to write everywhere this.props.classes? this.props.classes.button : '' in order to avoid "the value is possible undefined' compiler errors.

Please, update TS section in documentation. It really takes time to understand how it works.

@snk5000 I think you're making it a little more complicated than it needs to be. Your example should work just as well with the following changes:

import * as React from 'react';
import { WithStyles, Button, withStyles } from 'material-ui';
import { StyleRules, Theme } from 'material-ui/styles';

-const styleClasses = {
-  button: '',
-};

-const styles = (theme: Theme): StyleRules<keyof typeof styleClasses> => (
+const styles = (theme: Theme) => (
  { button: {
      backgroundColor: theme.palette.primary[200]
    }
  }
);

interface MyButtonProps {
  label: string;
}

-class MyButton extends React.Component< MyButtonProps & WithStyles<keyof typeof styleClasses>> {  
+class MyButton extends React.Component< MyButtonProps & WithStyles<'button'>> {
  render () {
-    const classes = this.props.classes ? this.props.classes : styleClasses;
+    const { classes } = this.props;
    return (
-      <Button className={classes.button}>this.props.label</Button>
+      <Button className={classes.button}>{this.props.label}</Button> // minor bug fix...
    );
  }
}

export default withStyles(styles)(MyButton);

In particular I'm confused about the ternary expression you had inside render()... that really shouldn't be necessary because classes will always be defined in a component wrapped by withStyles(). And the typing for the last few beta versions has been consistent with this, so you shouldn't be getting "the value is possible undefined" errors.

@pelotom

Thank you for the reply. The statement in render function was from my previous version of code, I used StyledComponentProps before, and I had to check if props.classes exists.

With your approach there is no type checking of styles return type. I always forget to add new key here -> <& WithStyles<'button'> when I add it to styles function. That's why I decided to store keys in a separate variable.

It would be very good if you update this guide: https://material-ui-1dab0.firebaseapp.com/guides/typescript with these examples. I'm new in TS and it was very hard to understand TS usage with MUI.

With your approach there is no type checking of styles return type.

I'm not sure what you mean, the class keys are inferred.

I always forget to add new key here -> <& WithStyles<'button'> when I add it to styles function.

If you forget you will be reminded very quickly, because it will be a type error if you try to use it.

That's why I decided to store keys in a separate variable.

That's a fine approach, but you don't need this:

const styleClasses = {
  button: '',
};

//...

class MyButton extends React.Component<MyButtonProps & WithStyles<keyof typeof styleClasses>> {

You can just write

const ClassKey = 'button';

//...

class MyButton extends React.Component<MyButtonProps & WithStyles<ClassKey>> {

It would be very good if you update this guide: https://material-ui-1dab0.firebaseapp.com/guides/typescript with these examples. I'm new in TS and it was very hard to understand TS usage with MUI.

I'm still not sure what this example shows that is not evinced by the guide. But the good news is you could update the guide just as easily as me :) Notice the button at the top of the page:

image

For the life of me can seem to find the correct solution to this problem. I keep getting an error that states typeof "my components name" is not assignable to type 'StatelessComponent', provides no match for the signature '(props: myprops & WithStyles<"" | "" |...>, and is not assignable to paramter of type"ComponentType

I've tried everything in this issue, also used your typescript docs using a decorator and nothing seems to work. I still get the same error...

Here is my code:

const styles = (theme: Theme) => ({
    selectField: {
        width: "180px",
        height: "48px",
    },...
});

interface SelectProps {
    id: string;
    displayValue: any;
    onChange: (event: React.ChangeEvent<HTMLInputElement>) => void | undefined;
    blue?: boolean;

}

class OurSelectFields extends React.Component<SelectProps & WithStyles<"selectField" | "" ...>>{
     render(){
          const { classes } = this.props;
          return( <MyComponent/>)
     }
}

export const SelectField = withStyles(styles)(OurSelectFields);

Couldn't find anything on stackoverflow and this seems to be the closest issue I could find.

@codepressd the code you have given compiles fine for me in both TypeScript 2.5.3 and 2.6.1, once I remove the ...s and replace <MyComponent /> (which you haven't defined) with <div />. I might be able to help you if you provide a minimal complete and self contained example which exhibits the error, along with the version of TypeScript you're using.

Thanks for getting back to me. I tried it again today with no luck... Here is my actual code.

const styles = (theme: Theme) => ({
    selectField: {
        width: "180px",
        height: "48px",
    },
    redSelectFieldUnderline: {
        "&:before": {
            height: "1px",
            backgroundColor: Colors.companyBrightRed(),
        },
        "& svg": {
            fill: Colors.companyBrightRed(),
        },
        "&:after": {
            backgroundColor: Colors.companyBrightRed(),
        },
    },
    blueSelectFieldUnderline: {
        "&:before": {
            height: "1px",
            backgroundColor: Colors.companyBlue,
        },
        "& svg": {
            fill: Colors.companyBlue,
        },
        "&:after": {
            backgroundColor: Colors.companyBlue,
        },
    },
    redSelectHoverHack: {
        width: "180px",
        "&:hover": {
            backgroundColor: "transparent",
            borderBottom: `2px solid ${Colors.companyBrightRed()}`
        },
        "&:focus": {
            outline: "none",
            backgroundColor: "transparent",
            borderBottom: `2px solid ${Colors.companyBrightRed()}`
        }
    },
    blueSelectHoverHack: {
        width: "180px",
        "&:hover": {
            backgroundColor: "transparent",
            borderBottom: `2px solid ${Colors.companyBlue}`
        },
        "&:focus": {
            outline: "none",
            backgroundColor: "transparent",
            borderBottom: `2px solid ${Colors.companyBlue}`
        }
    },
    menuWrapStyles: {
        top: "72px"
    }
});

interface SelectProps {
    id: string;
    displayValue: any;
    onChange: (event: React.ChangeEvent<HTMLInputElement>) => void | undefined;
    blue?: boolean;

}

class OurSelectFields extends React.Component<SelectProps & WithStyles<"selectField" | "blueSelectField" | "redSelectField" | "menuWrapStyles" | "blueSelectHoverHack" | "redSelectHoverHack">, never>{

    render() {
        const { classes } = this.props;
        return (
            <TextField
                id={this.props.id}
                select
                className={classes.selectField}
                InputClassName={this.props.blue ? classes.blueSelectField : classes.redSelectField}
                SelectProps={{
                    MenuProps: {
                        className: classes.menuWrapStyles,
                    }
                }}
                inputClassName={this.props.blue ? classes.blueSelectHoverHack : classes.redSelectHoverHack}
                onChange={this.props.onChange}
                value={this.props.displayValue}
            >
                {this.props.children}

            </TextField>
        )
    }
};

export const SelectField = withStyles(styles)(OurSelectFields);

I'm using typescript 2.5.2 so I'll upgrade and see if that fixes it.
Thanks for your help

edit --- I still get the same error with typescript 2.5.3

@pelotom So I dug into this today and it has to do with classes being required in my interface...

// This works but have to pass an empty object into the component
interface mycomponentProps{
    classes: object
}
// This fails
interface mycomponentProps{
    classes?: object
}

I'm wondering if there is a cleaner way to make this work.

Thanks again for your help!!

@codepressd I'm not seeing the error you describe, but I'm seeing a different error because the class keys in styles don't match those in your WithStyles<...>.

@pelotom @snk5000
I think your default export should look like this :

- export default withStyles(styles)(MyButton);
+ export default withStyles(styles)<MyButtonProps>(MyButton);

Following that I have another issue, how do use that with another hoc. mainly how can it be integrated with composefrom recompose:
for example if we had to add another HOC, the following doesn't work :

const enhance = compose(withWidth(), withStyles(styles)<MyButtonProps>);

export default enhance(MyButton);

@stunaz this is not a valid TypeScript expression:

withStyles(styles)<MyButtonProps>

It should just be

withStyles(styles)

and type inference should be able to specialize the result appropriately. Beyond that, I can't really say what might be wrong because I don't have the complete code. If you still have problems, please post a complete, self-contained example as well as the error you're getting.

Hi @pelotom, here is my use case:

// Home.tsx
import * as React from 'react';
import Hello from './Hello';

interface IHomeProps {}

export default class Home extends React.Component<IHomeProps> {
    public render() {
        return <Hello name="Bob" />;
    }
}
// hello.tsx
import * as React from 'react';
import { Grid } from 'material-ui';
import withWidth, { WithWidthProps } from 'material-ui/utils/withWidth';
import withStyles, { WithStyles } from 'material-ui/styles/withStyles';
import { compose } from 'redux';
import { Theme } from 'material-ui/styles';

const styles = (theme: Theme) => ({
    root: {
        backgroundColor: theme.palette.common.faintBlack
    }
});

interface IHelloProps {
    name?: string;
}

export class Hello extends React.Component<IHelloProps & WithWidthProps & WithStyles<'root'>> {
    public static defaultProps = {
        name: 'Alex'
    };

    public render() {
        return (
            <Grid
                className={this.props.classes.root}
                direction={this.props.width === 'sm' ? 'column' : 'row'}
            >
                <h1>Hello {this.props.name}!</h1>
            </Grid>
        );
    }
}

const enhance = compose(withWidth(), withStyles(styles));

export default enhance(Hello);

The error I have on Home.tsxis the following

Type '{ name: "Bob"; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<Component<WithWidthProps, ComponentState>> & Reado...'.
  Type '{ name: "Bob"; }' is not assignable to type 'Readonly<WithWidthProps>'.
    Property 'width' is missing in type '{ name: "Bob"; }'.

Taking compose out of the equation, since Redux typings are questionable, does this work any better?

export default withWidth()(withStyles(styles)(Hello))

same issue ...
event tried with

export default withWidth()(withStyles(styles)<IHelloProps>(Hello));

having the exact same error

@stunaz if you change the definition of withWidth to this:

export default function withWidth(
  options?: WithWidthOptions
): <P>(
  component: React.ComponentType<P & WithWidthProps>
) => React.ComponentClass<P & Partial<WithWidthProps>>;

does it work?

While overwriting the withWidth in my node_modules with the code above , I now get

Argument of type 'ComponentType<IHelloProps & StyledComponentProps<"root">>' is not assignable to parameter of type 'ComponentType<IHelloProps & StyledComponentProps<"root"> & WithWidthProps>'.
  Type 'ComponentClass<IHelloProps & StyledComponentProps<"root">>' is not assignable to type 'ComponentType<IHelloProps & StyledComponentProps<"root"> & WithWidthProps>'.
    Type 'ComponentClass<IHelloProps & StyledComponentProps<"root">>' is not assignable to type 'StatelessComponent<IHelloProps & StyledComponentProps<"root"> & WithWidthProps>'.
      Type 'ComponentClass<IHelloProps & StyledComponentProps<"root">>' provides no match for the signature '(props: IHelloProps & StyledComponentProps<"root"> & WithWidthProps & { children?: ReactNode; }, context?: any): ReactElement<any>'.

Tried with another HOC different from withWidth, it worked, you on the right path, the issue seems to be with withWidth

@stunaz is that error when using compose still or with

export default withWidth()(withStyles(styles)(Hello))

?

All right, you last merged fixed it. i.e

A: the following works 馃挴

export default withWidth()(withStyles(styles)(Hello))

B: The following still don't works

const enhance = compose(withWidth(), withStyles(styles));
export default enhance(Hello);

throwing:

Type '{ name: "Bob"; }' has no properties in common with type 'IntrinsicAttributes & IntrinsicClassAttributes<Component<Partial<WithWidthProps>, ComponentState>...'.

So I guess the issue is now in redux / compose typing land

FWIW, even with perfect typings I think TypeScript in general has trouble inferring the types needed to compose 2 generic functions, so you probably need to pass explicit type parameters to compose... or, just do it like above 馃槃

I will stick with whatever works
Thanks for your help @pelotom , much appreciated

Closing since the original issue should be fixed by https://github.com/DefinitelyTyped/DefinitelyTyped/pull/20987.

Hey there, i fall in this issue many time and i have just found a solution that i thought, match your issue @stunaz.
First, i've created my decorate variable:

type IMyButtonProps = MyButtonProps &  WithStyles<"button">;
const decorate = withStyles((theme: Theme) => ({
  button: {
    margin: theme.spacing.unit
  }
})) 

then i export like this :

export default compose(connect(mapStateToProps))(decorate<IMyButtonProps>(MyButton));

No need to had theme or classes property optional or of type any in your props.
So maybe for your case @stunaz

export default compose(withWidth(), connect(mapStateToProps))(decorate<IMyButtonProps>(MyButton));
/// If not working try this:
const MyButtonWithWidth = withWidth()(MyButton)
export default compose(connect(mapStateToProps))(decorate<IMyButtonProps>(MyButtonWithWidth));

I thought that the documentation about Typescript in material ui web site is clear but need a better explaination for class component with redux, compose, withStyles and withTheme all in one.

Hi @Vandell63 , I gave up on typescript, went back to flow. But I think the solutions in this thread are outdated. they were a lot of changements. you should file another issue.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

FranBran picture FranBran  路  3Comments

mb-copart picture mb-copart  路  3Comments

ghost picture ghost  路  3Comments

anthony-dandrea picture anthony-dandrea  路  3Comments

mattmiddlesworth picture mattmiddlesworth  路  3Comments