Material-ui: [Typescript] Question: Generic type arguments in JSX elements working with withStyles

Created on 19 Jun 2018  路  8Comments  路  Source: mui-org/material-ui

  • [x] This is a v1.x issue (v0.x is no longer maintained).
  • [x] I have searched the issues of this repository and believe that this is not a duplicate.

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?

Your Environment

| Tech | Version |
|--------------|---------|
| Material-UI | v1.2.2 |
| React | 16.4.1 |
| browser | |
| etc | |

typescript

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.

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} />;
  }
}

All 8 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?

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} />;
  }
}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

ghost picture ghost  路  3Comments

activatedgeek picture activatedgeek  路  3Comments

mattmiddlesworth picture mattmiddlesworth  路  3Comments

ghost picture ghost  路  3Comments

reflog picture reflog  路  3Comments