This is related to #10357. When declaring the props interface for a given component, the current recommendation in the docs (https://material-ui.com/guides/typescript/#usage-of-withstyles) is:
import { WithStyles, createStyles } from '@material-ui/core';
const styles = (theme: Theme) => createStyles({
root: { /* ... */ },
paper: { /* ... */ },
button: { /* ... */ },
});
interface Props extends WithStyles<typeof styles> {
foo: number;
bar: boolean;
}
However, this creates a bit of an issue if you export the Props interface to be used by other modules. For example:
const foo: Props = {
foo: 42,
bar: true,
};
results in an error:
Property 'classes' is missing in type '{ foo: number; bar: true; }' but required in type 'Props'.ts(2741)
I understand the reasoning for wanting it to be a required property from the internal component perspective so that the component knows it will always have a classes prop provided, but from an external component API perspective it makes it quite clunky to use. For example if i had another component and followed a typical MUI convention:
interface OtherProps {
ChildComponentProps?: Props;
}
it would require the consumer to provide a classes property if they specified the ChildComponentProps prop.
We've worked around this in our applications by creating a new type called WithStylesPublic that is a copy of WithStyles but makes the classes property optional. Then we use it like so:
interface Props extends WithStylesPublic<typeof styles> {
foo: number;
bar: boolean;
}
const Component: React.FC<Props & WithStyles<typeof styles>> = ({ classes, ...other }) => { ... });
Not sure on the best solution here. Should MUI provide an interface similar to what i've described with WithStylesPublic that has classes optional? Should this just be documentation? If so it leaves the burden on the developer to work around this case.
We have had great success creating an internal component library that wraps the MUI components and re-exports them. In some cases we like to add additional functionality to those components such as new props, but in those cases we run into the issues described above.
| Tech | Version |
| ----------- | ------- |
| Material-UI | v4.4.2 |
| React | |
| Browser | |
| TypeScript | v3.6.2 |
| etc. | |
@ianschmitz Thanks for the report. Did you look into a potential solution?
I've been using the WithStylesPublic work around with success for a while now, but I'm not sure what the best course of action is for MUI itself.
Perhaps approaching it from the perspective of "what is the story" for creating generic components that wrap MUI components in TypeScript?
Another issue we've ran into when using this pattern is dealing with the classes prop.
If you do something like:
import MUIFormControl, {
FormControlClassKey as MUIFormControlClassKey,
FormControlProps as MUIFormControlProps,
} from "@material-ui/core/FormControl";
const styles = createStyles({
inlineHelp: {
position: "absolute",
top: "0.25rem",
right: 0,
},
});
const FormControl = React.forwardRef<HTMLDivElement, FormControlProps & WithStyles<typeof styles>>(
function FormControl(props, ref) {
const {
children,
classes: { inlineHelp: inlineHelpClass, ...otherClasses },
...other
} = props;
return (
<MUIFormControl {...other} classes={otherClasses} ref={ref}>
// ...
}
);
export default withStyles(styles)(FormControl);
The type for classes will have the classes from MUI FormControl, as well as my additional class inlineHelp. However when trying to pass values for the classes "inherited" from FormControl, we get a MUI error saying the style sheet doesn't know about the class.
To work around what I did was:
export type FormControlClassKey = MUIFormControlClassKey | "inlineHelp";
const styles = createStyles<FormControlClassKey, {}>({
inlineHelp: {
position: "absolute",
top: "0.25rem",
right: 0,
},
// Hoist MUI classes
fullWidth: {},
marginDense: {},
marginNormal: {},
root: {},
});
Which re-defines all of the classes of the wrapped component, so to the consumer they can still provide class names for all of the MUI FormControl classes.
Hi, i have the same problem. I think better solution is to change withStyles typing to declare classes prop as optional for resulting HOC.
Is looks like there is something done in this way with PropInjector, but it is not working, because mandatory classes property is already in original Props too.
Simple solution could be following:
export default function withStyles<
ClassKey extends string,
Options extends WithStylesOptions<Theme> = {},
Props extends object = {},
>(
style: Styles<Theme, Props, ClassKey>,
options?: Options,
): PropInjector<
WithStyles<ClassKey, Options['withTheme']>,
// StyledComponentProps<ClassKey> & CoerceEmptyInterface<Props>
StyledComponentProps<ClassKey> & CoerceEmptyInterface<Omit<Props, keyof WithStyles<ClassKey, Options['withTheme']>>>
>;
It could possibly "break" Props typing if someone adds classes prop as a mandatory intentionally, but it is IMO better then current typing with declaring classes as mandatory all the time.
I don't think we can do much here other than document how to workaround.
The problem is that you have two components here: the actual implementation and the one decorated with classes. What we usually define as the Props in that context is the types for the props that are available in the implementation. This isn't the type you should export but rather the type of the props of the decorated one.
Granted that the naming might not be perfect but the props in the implementation should extend WithStyles while the exported props should extend StyledComponentProps.
import { withStyles, StyledComponentProps, WithStyles } from '@material-ui/core/styles'
const styles = {};
interface ComponentProps {}
function Component(props: ComponentProps & WithStyles<typeof styles>) {
return null;
}
export type Props = ComponentProps & StyledComponentProps<typeof styles>;
export default withStyles(styles)(Component);
Update on https://github.com/mui-org/material-ui/issues/17491#issuecomment-580628311:
I just realized that we already have a helper type for this: StyledComponentProps:
import { withStyles, StyledComponentProps, WithStyles } from '@material-ui/core/styles'
const styles = {};
interface ComponentProps {}
function Component(props: ComponentProps & WithStyles<typeof styles>) {
return null;
}
export type Props = ComponentProps & StyledComponentProps<typeof styles>;
export default withStyles(styles)(Component);
So close @eps1lon!
Unfortunately the generic type of WithStyles and StyledComponentProps are slightly different, which causes an error:
Type '({ palette }: Theme) => Record<CheckboxClassKey, CSSProperties | CreateCSSProperties<{}> | ((props: {}) => CreateCSSProperties<{}>)>' does not satisfy the constraint 'string'.
Looks like WithStyles types the generic as
StylesOrClassKey extends string | Styles<any, any, any> = string
while StyledComponentProps types the generic as
ClassKey extends string = string
I imagine that is because WithStyles lives in @material-ui/core, while the other lives in @material-ui/styles?
@ianschmitz Sorry this is a bit hard to follow. Is my example not working or is this error from your code?
No worries.
Your example doesn't seem to work. Here's a more concrete example of what i've tried:
export type FormControlClassKey = MUIFormControlClassKey | "foo";
const styles = createStyles<FormControlClassKey, {}>({
foo: {
position: "absolute",
top: "0.25rem",
right: 0,
},
// Hoist MUI classes. Necessary to allow pass through of MUI classes
fullWidth: {},
marginDense: {},
marginNormal: {},
root: {},
});
export type FormControlProps = MUIFormControlProps &
StyledComponentProps<typeof styles>;
const FormControl = React.forwardRef<HTMLDivElement, FormControlProps & WithStyles<typeof styles>>(
);
StyledComponentProps<typeof styles> gives error:
Type 'Record<FormControlClassKey, CSSProperties | CreateCSSProperties<{}> | PropsFunc<{}, CreateCSSProperties<{}>>>' does not satisfy the constraint 'string'
@ianschmitz @eps1lon I have an issue which I believe is the same one. I created a CodeSandBox to demonstrate.
Here is an explanation of the problem:
I have just one exported component, called BaseChip. To construct that component I am using MUI's withStyles, passing in a styles function and calling the result with a component called BaseChipUnstyled that will have the classes prop injected.
The exported component should only take one mandatory prop label and two optional props backgroundColor and color; it should not have to take the classes prop. However, when I try to use BaseChip in App.tsx, there is an error that the classes prop is not provided.
As far as I can tell, the code below strictly follows the relevant section of the docs
// BaseChip.tsx
import React from 'react';
import { createStyles, Theme, WithStyles, Chip, withStyles } from '@material-ui/core';
const BaseChipStyles = (theme: Theme) =>
createStyles({
root: ({ backgroundColor = 'yellow' }: BaseChipProps) => ({ backgroundColor }),
label: ({ color = 'blue' }: BaseChipProps) => ({ color }),
});
interface BaseChipProps extends WithStyles<typeof BaseChipStyles> {
label: string;
backgroundColor?: 'yellow' | 'orange';
color?: 'blue' | 'green';
}
const BaseChipUnstyled = ({ label, classes }: BaseChipProps) => <Chip label={label} classes={classes} />;
export const BaseChip = withStyles(BaseChipStyles)(BaseChipUnstyled);
// App.tsx
import React from 'react';
import { BaseChip } from './components/BaseChip';
function App() {
return (
<div className="App">
<BaseChip label={'MyLabel'} />
</div>
);
}
export default App;
What's going on here, what am I missing?
@goepi, your example has a circular reference between your BaseChipStyles and BaseChipProps. I believe this caused your type error.
Most helpful comment
@ianschmitz @eps1lon I have an issue which I believe is the same one. I created a CodeSandBox to demonstrate.
Here is an explanation of the problem:
I have just one exported component, called
BaseChip. To construct that component I am using MUI'swithStyles, passing in a styles function and calling the result with a component calledBaseChipUnstyledthat will have theclassesprop injected.The exported component should only take one mandatory prop
labeland two optional propsbackgroundColorandcolor; it should not have to take theclassesprop. However, when I try to use BaseChip in App.tsx, there is an error that the classes prop is not provided.As far as I can tell, the code below strictly follows the relevant section of the docs
What's going on here, what am I missing?