Skip to content
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

Closed
2 tasks done
franklixuefei opened this issue Jun 19, 2018 · 9 comments
Closed
2 tasks done

Comments

@franklixuefei
Copy link
Contributor

franklixuefei commented Jun 19, 2018

  • This is a v1.x issue (v0.x is no longer maintained).
  • 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
@franklixuefei
Copy link
Contributor Author

franklixuefei commented Jun 19, 2018

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.

@jasanst
Copy link

jasanst commented Jun 25, 2018

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?

@franklixuefei
Copy link
Contributor Author

@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] : []}
/>

@jasanst
Copy link

jasanst commented Jun 26, 2018

Thanks, I was incorrect extending the props at defintion with WithStyles when it just needed to be extended on the props of the class.

@Jocaetano
Copy link
Contributor

Is there any better way to do this? I am not a fan of outputting more javascript to fix type errors.

@MastroLindus
Copy link

I agree with @Jocaetano there should be a better way to handle this at the type level instead of requiring runtime workarounds

@lukas-zech-software
Copy link

lukas-zech-software commented Aug 17, 2018

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?

mattmccutchen added a commit to mattmccutchen/material-ui that referenced this issue Sep 30, 2018
component to a generic component.

TODO: Move example from Stack Overflow to examples.

Fixes mui#11921.
@mattmccutchen
Copy link
Contributor

mattmccutchen commented Sep 30, 2018

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

@mauroc8
Copy link

mauroc8 commented Jan 28, 2022

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants