-
-
Notifications
You must be signed in to change notification settings - Fork 32.2k
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
[Typescript] Question: Generic type arguments in JSX elements working with withStyles #11921
Comments
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? |
component to a generic component. TODO: Move example from Stack Overflow to examples. Fixes mui#11921.
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} />;
}
} |
I'm surprised noone posted this solution, that seems simpler function MyComponent<T>(props: MyComponentProps<T> & { classes: MyComponentClasses }) {
...
}
const MyComponentWithStyles = withStyles(styles)(MyComponent);
export default function MyGenericComponent<T>(props: MyComponentProps<T>) {
return <MyComponentWithStyles {...props} />;
} |
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.However, after doing this, when I call
MyComponent
like below, it said thatMyComponent
expects 0 type arguments, but provided 1. It seems like{}
was passed toMyComponent
as the default type parameter if none is specified.So, my question is how I can achieve generic type arguments in JSX element with HOC?
Your Environment
The text was updated successfully, but these errors were encountered: