I wanted to leverage the generic type argument in JSX elements feature introduced in ts 2.9.1. However, when it comes to exporting with withStyles, I don't know how to expose that generic type parameter to the outside world.
class MyComponent<T> extends React.PureComponent<MyComponentProps<T>> {...}
export default withStyles(styles)(MyComponent);
However, after doing this, when I call MyComponent like below, it said that MyComponent expects 0 type arguments, but provided 1. It seems like {} was passed to MyComponent as the default type parameter if none is specified.
<MyComponent<string> prop1={...} /> // error: expected 0 type arguments, but provided 1.
So, my question is how I can achieve generic type arguments in JSX element with HOC?
| Tech | Version |
|--------------|---------|
| Material-UI | v1.2.2 |
| React | 16.4.1 |
| browser | |
| etc | |
Okay, found a solution:
export default class<T> extends React.PureComponent<MyComponentProps<T>> {
private readonly C = this.wrapperFunc();
render() {
return <this.C {...this.props} />;
}
private wrapperFunc() {
type t = new() => MyComponent<T>;
return withStyles(styles)(MyComponent as t);
}
}
Essentially, I have to write a wrapper class that wraps around the HOC'ed component.
Do you have an example of this working? When I try it the styled component compiles fine but actually using it makes TypeScript complain about missing classes property?
@jasanst Take a look at a complete example below.
Note that this is the real code from my project. No modification made yet.
import Fade from '@material-ui/core/Fade';
import Paper from '@material-ui/core/Paper';
import { Theme } from '@material-ui/core/styles/createMuiTheme';
import withStyles, { CSSProperties, WithStyles } from '@material-ui/core/styles/withStyles';
import Typography from '@material-ui/core/Typography';
import { ControllerStateAndHelpers } from 'downshift';
import * as React from 'react';
import AutocompleteMenuItem from './AutocompleteMenuItem';
const styles = ({ palette, spacing, zIndex }: Theme) => ({
menu: {
position: 'relative',
zIndex: zIndex.drawer + 1
} as CSSProperties,
paper: {
position: 'absolute',
zIndex: zIndex.modal,
width: '100%',
maxHeight: 400,
overflow: 'auto',
} as CSSProperties,
listContainer: {
position: 'relative',
overflowY: 'auto',
backgroundColor: 'inherit',
} as CSSProperties,
noMatch: {
padding: '8px 16px 8px 24px',
fontStyle: 'italic',
color: palette.text.disabled
} as CSSProperties,
});
export interface IAutocompleteMenuProps<TItem> {
downshiftControllerStateAndHelpers: ControllerStateAndHelpers<TItem>;
items: TItem[];
selectedItems: TItem[];
noMatchText: string;
loading: boolean;
loadingText: string;
}
// TODO: if we want to enable async menu content loading, then we need to execute
// clearItems() on data retrieval.
// https://github.com/mui-org/@material-ui/core/issues/10657
// https://codesandbox.io/s/github/kentcdodds/advanced-downshift
class AutocompleteMenu<TItem> extends React.PureComponent<IAutocompleteMenuProps<TItem> & WithStyles<typeof styles>> {
constructor(props: IAutocompleteMenuProps<TItem> & WithStyles<typeof styles>) {
super(props);
this.renderMenuItems = this.renderMenuItems.bind(this);
this.renderMenu = this.renderMenu.bind(this);
}
render() {
const { downshiftControllerStateAndHelpers, classes } = this.props;
const { isOpen, getMenuProps } = downshiftControllerStateAndHelpers;
return (
<Fade in={isOpen} mountOnEnter unmountOnExit>
<div
className={classes.menu}
{...getMenuProps({
'aria-label': 'autocompletion menu'
})}
>
<Paper classes={{ root: classes.paper }}>
<div className={classes.listContainer}>
{this.renderMenu()}
</div>
</Paper>
</div>
</Fade>
);
}
private renderMenuItems(items: TItem[]) {
const { downshiftControllerStateAndHelpers, selectedItems } = this.props;
const { highlightedIndex, itemToString } = downshiftControllerStateAndHelpers;
return items.map((item, index) => {
return (
<AutocompleteMenuItem<TItem>
index={index}
highlighted={index === highlightedIndex}
selected={selectedItems.some(
(selectedItem) => itemToString(selectedItem).toLowerCase() === itemToString(item).toLowerCase())}
downshiftControllerStateAndHelpers={downshiftControllerStateAndHelpers}
item={item}
key={index}
/>
);
});
}
private renderMenu() {
const { classes, noMatchText, items } = this.props;
if (items.length === 0) {
return (
<div className={classes.noMatch}>
<Typography color={'inherit'} noWrap>{noMatchText}</Typography>
</div>
);
}
return (
<>
{this.renderMenuItems(items)}
</>
);
}
}
// tslint:disable-next-line:max-classes-per-file
export default class<T> extends React.PureComponent<IAutocompleteMenuProps<T>> {
private readonly C = this.wrapperFunc();
render() {
return <this.C {...this.props} />;
}
private wrapperFunc() {
type t = new() => AutocompleteMenu<T>;
return withStyles(styles)(AutocompleteMenu as t);
}
}
Usage:
<AutocompleteMenu<TItem>
downshiftControllerStateAndHelpers={stateAndHelpers}
noMatchText={noMatchText || 'No results found'}
items={filteredCandidates}
loading={loading || false}
loadingText={loadingText || 'Loading...'}
selectedItems={selectedItem ? [selectedItem] : []}
/>
Thanks, I was incorrect extending the props at defintion with WithStyles when it just needed to be extended on the props of the class.
Is there any better way to do this? I am not a fan of outputting more javascript to fix type errors.
I agree with @Jocaetano there should be a better way to handle this at the type level instead of requiring runtime workarounds
This is a tad shorter than @franklixuefei solution
// ./common/FormField
const styles = (theme: Theme) => createStyles({
input: {
margin: theme.spacing.unit,
},
});
interface Props<T> {
property: keyof T;
locale: Locale<T>;
}
export default function wrap<T>(props: Props<T>): ReactElement<Props<T>> {
const A = withStyles(styles)(
class FormField<T> extends React.Component<Props<T> & WithStyles<typeof styles>> {
public render(): JSX.Element {
const {classes, property, locale} = this.props;
const label = locale[property];
return (
<TextField
fullWidth
label={label}
className={classes.input}
inputProps={{'aria-label': label}}
/>
);
}
},
) as any;
return React.createElement(A, props);
}
interface Company {
address: string;
}
// USAGE
import FormField from './common/FormField';
const usage = <FormField<Company> property={'address'} locale={locale.registrationForm.formFields}/>;
But still there needs to be a better solution. As @MastroLindus said, it is not really viable to solve typing problems with runtime workarounds!
@oliviertassinari Is there a proposed solution to this? If not, can we please reopen this issue or create a new one?
Edit: I am now recommending the following solution based on franklixuefei's solution. It doesn't require a change to Material UI. See the additional remarks on Stack Overflow.
class WrappedBaseFormCard<T> extends React.Component<
// Or `PropsOf<WrappedBaseFormCard<T>["C"]>` from @material-ui/core if you don't mind the dependency.
WrappedBaseFormCard<T>["C"] extends React.ComponentType<infer P> ? P : never,
{}> {
private readonly C = withStyles(styles)(
// JSX.LibraryManagedAttributes handles defaultProps, etc. If you don't
// need that, you can use `BaseFormCard<T>["props"]` or hard-code the props type.
(props: JSX.LibraryManagedAttributes<typeof BaseFormCard, BaseFormCard<T>["props"]>) =>
<BaseFormCard<T> {...props} />);
render() {
return <this.C {...this.props} />;
}
}
Most helpful comment
Edit: I am now recommending the following solution based on franklixuefei's solution. It doesn't require a change to Material UI. See the additional remarks on Stack Overflow.