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

[docs] TypeScript Guide #8598

Closed
sebald opened this issue Oct 9, 2017 · 25 comments
Closed

[docs] TypeScript Guide #8598

sebald opened this issue Oct 9, 2017 · 25 comments
Labels
docs Improvements or additions to the documentation typescript

Comments

@sebald
Copy link
Member

sebald commented Oct 9, 2017

Following this suggestions by @oliviertassinari, let's add some information about using TypeScript with material-ui. I would love to hear from folks what issues they ran into when they start using the v1 branch.

I'll update this post with suggestions and create a separate PR containing an initial guide for the TS usage 🙂


Stuff to include in the guide:

  • How to get the typings (this of course contains zero steps/setup, but I think this is worth mentioning so people do not accidentally install @types/material-ui).
  • How to use withStyles as decorator / or why you really can't.
  • How to report issues with the typings.
@sebald
Copy link
Member Author

sebald commented Oct 9, 2017

@oliviertassinari @pelotom ping! 😎 Any suggestions?

@oliviertassinari oliviertassinari added docs Improvements or additions to the documentation typescript labels Oct 9, 2017
@oliviertassinari
Copy link
Member

This sounds great. I can't think of anything else.

@pelotom
Copy link
Member

pelotom commented Oct 9, 2017

It sounds good to me, although I'm pretty busy this week and may not be able to get to it in the near term...

@cfilipov
Copy link

I would also appreciate it if we could also have a typescript version of the example code.

@lukeggchapman
Copy link
Contributor

I attempted to use withStlyes today with typescript, and failed. A basic example would be appreciated. The following fails:

import React from 'react'
import { withStyles, WithStyles } from 'material-ui/styles'

const styles = {
    test: {
        color: 'black',
        backgroundColor: 'white',
    },
}

class Component extends React.Component<WithStyles<keyof typeof styles>> {
    render() {
        return <div className={this.props.classes.test} />
    }
}

const ComponentWithStyles = withStyles(Component)

class Consumer extends React.Component {
    render() {
        return (
            <div>
                <Component /> // Fails with "Property 'classes' is missing in type '{}'."
            </div>
        )
    }
}

Also when styles is a function is there a solution for getting the types from the returned object, or do we have to be explicit when calling WithStyles<> (Eg. WithStyles<'test'>)?

@chilbi
Copy link

chilbi commented Oct 13, 2017

stateless functional component

import * as React from 'react';
import { StyleRulesCallback, WithStyles, withStyles } from 'material-ui/styles';
import Badge from 'material-ui/Badge';
import Icon from 'material-ui/Icon';

type C = 'badge';

interface P { }

const styles: StyleRulesCallback<C> = (theme) => ({
  badge: {
    margin: `0 ${theme.spacing.unit * 2}px`,
  },
});

function BadgeDemo({ classes }: P & WithStyles<C>) {
  return (
    <div>
      <Badge className={classes.badge} badgeContent={4} color="primary">
        <Icon>mail</Icon>
      </Badge>
      <Badge className={classes.badge} badgeContent={10} color="accent">
        <Icon>folder</Icon>
      </Badge>
    </div>
  );
}

export default withStyles(styles)(BadgeDemo);

anonymous stateless functional component

import * as React from 'react';
import { withStyles } from 'material-ui/styles';
import AppBar from 'material-ui/AppBar';
import Toolbar from 'material-ui/Toolbar';
import Typography from 'material-ui/Typography';
import IconButton from 'material-ui/IconButton';
import Icon from 'material-ui/Icon';
import Button from 'material-ui/Button/Button';

const ButtonAppBar = withStyles((theme) => ({
  root: {
    marginTop: theme.spacing.unit * 3,
    width: '100%',
  },
  menuButton: {
    marginLeft: -12,
    marginRight: 20,
  },
  loginButton: {
    marginLeft: 'auto',
  },
}))(({ classes }) => (
  <div className={classes.root}>
    <AppBar position="static">
      <Toolbar>
        <IconButton className={classes.menuButton} color="contrast" aria-label="menu">
          <Icon>menu</Icon>
        </IconButton>
        <Typography type="title" color="inherit">
          Title
        </Typography>
        <Button className={classes.loginButton} color="contrast">
          Login
        </Button>
      </Toolbar>
    </AppBar>
  </div>
));

export default ButtonAppBar;

class component

import * as React from 'react';
import { withStyles, WithStyles } from 'material-ui/styles';
import List, {
  ListItem,
  ListItemText
} from 'material-ui/List';
import Menu, { MenuItem } from 'material-ui/Menu';
import { StyledComponent } from 'material-ui';

type C = 'root';

interface P { 
  options?: string[];
}

interface S {
  open: boolean;
  anchorEl?: HTMLButtonElement;
  selectedIndex: number;
}

@withStyles<C>((theme) => ({
  root: {
    maxWidth: 360,
    width: '100%',
    backgroundColor: theme.palette.background.paper,
  },
}))
class SelectedMenu extends React.Component<P & WithStyles<C>, S> {
  static defaultProps: Partial<P & WithStyles<C>> = {
    options: [
      'Show all notification content',
      'Hide sensitive notification content',
      'Hide all notification content',
    ],
  };

  constructor(...args) {
    super(...args);
    this.state = {
      open: false,
      selectedIndex: 0,
    };
  }

  handleListItemClick = (event: React.MouseEvent<{}>) => {
    this.setState({
      open: true,
      anchorEl: event.currentTarget as HTMLButtonElement,
    });
  }

  handleRequestClose = () => {
    this.setState({ open: false });
  }

  createHandleMenuItemClick = (selectedIndex: number): React.MouseEventHandler<{}> => () => {
    this.setState({
      selectedIndex,
      open: false,
    });
  }

  render() {
    const id = 'lock-menu';
    const { classes, options } = this.props;
    const { selectedIndex, open, anchorEl } = this.state;
    return (
      <div className={classes.root}>
        <List>
          <ListItem
            button
            aria-haspopup={true}
            aria-controls={id}
            aria-label="When device is locked"
            onClick={this.handleListItemClick}
          >
            <ListItemText
              primary="When device is locked"
              secondary={options![selectedIndex]}
            />
          </ListItem>
        </List>
        <Menu
          id={id}
          open={open}
          anchorEl={anchorEl}
          onRequestClose={this.handleRequestClose}
        >
          {options!.map((option, i) => (
            <MenuItem
              key={option}
              selected={i === selectedIndex}
              onClick={this.createHandleMenuItemClick(i)}
            >
              {option}
            </MenuItem>
          ))}
        </Menu>
      </div>
    );
  }
}

export default SelectedMenu as StyledComponent<P, C>;

@pelotom
Copy link
Member

pelotom commented Oct 13, 2017

@chilbi

@withStyles<C>((theme) => ({
  root: {
    maxWidth: 360,
    width: '100%',
    backgroundColor: theme.palette.background.paper,
  },
}))
class SelectedMenu extends React.Component<P & WithStyles<C>, S> {

TypeScript class decorators are no longer supported. Instead I would write this as

const SelectedMenu = withStyles(theme => ({
  root: {
    maxWidth: 360,
    width: '100%',
    backgroundColor: theme.palette.background.paper,
  },
}))<P>(class extends React.Component<P & WithStyles<C>, S> {

@chilbi
Copy link

chilbi commented Oct 14, 2017

@pelotom
The above example is I use the v1.0.0-beta.15 StyledComponentDecorator to write.

// v1.0.0-beta.15
interface StyledComponentDecorator<ClassKey extends string = string> {
  // StatelessComponent
  <P>(
    component: React.StatelessComponent<P & WithStyles<ClassKey>>
  ): StyledComponent<P, ClassKey>;
  // ComponentClass
  <P, C extends React.ComponentClass<P & StyledComponentProps<ClassKey>>>(
    component: C
  ): C;
}

// v1.0.0-beta.16
// interface StyledComponentDecorator<ClassKey extends string = string> {
//   <P>(
//     component: React.ComponentType<P & WithStyles<ClassKey>>
//   ): StyledComponent<P, ClassKey>;
// }

export default function withStyles<ClassKey extends string>(
  style: StyleRules<ClassKey> | StyleRulesCallback<ClassKey>,
  options?: WithStylesOptions
): StyledComponentDecorator<ClassKey>;

Requires type assertion or type definition:

  • Type assertion in an expression:
import * as React from 'react';
import { withStyles, WithStyles } from 'material-ui/styles';
import { StyledComponent } from 'material-ui';

type C = 'root' | 'foo';

interface P { options?: string[]; }

interface S { open: boolean; }

export default withStyles((theme) => ({
  root: {},
  foo: {},
}))(class extends React.Component<P & WithStyles<C>, S> {
  render() {
    return (
      <div className={this.props.classes.root} />
    );
  }
}) as StyledComponent<P, C>; // type assertion
  • Type assertion when exporting
import * as React from 'react';
import { withStyles, WithStyles } from 'material-ui/styles';
import { StyledComponent } from 'material-ui';

type C = 'root' | 'foo';

interface P { options?: string[]; }

interface S { open: boolean; }

const Component = withStyles((theme) => ({
  root: {},
  foo: {},
}))(class extends React.Component<P & WithStyles<C>, S> {
  render() {
    return (
      <div className={this.props.classes.root} />
    );
  }
});

export default Component as StyledComponent<P, C>;  // type assertion
  • Explicit definition type
const Component: StyledComponent<P, C> = withStyles((theme) => ({  // type definition
  root: {},
  foo: {},
}))(class extends React.Component<P & WithStyles<C>, S> {
  render() {
    return (
      <div className={this.props.classes.root} />
    );
  }
});

export default Component;
  • Use decorator
import * as React from 'react';
import { withStyles, WithStyles } from 'material-ui/styles';
import { StyledComponent } from 'material-ui';

type C = 'root' | 'foo';

interface P { options?: string[]; }

interface S { open: boolean; }

@withStyles((theme) => ({
  root: {},
  foo: {},
}))
class Component extends React.Component<P & WithStyles<C>, S> {
  render() {
    return (
      <div className={this.props.classes.root} />
    );
  }
}

export default Component as StyledComponent<P, C>; // type assertion

@NuclleaR
Copy link

@chilbi I user your hook for decorator and getting error
error TS2605: JSX element type 'StyledComponentDecorator<{}>' is not a constructor function for JSX elements.
Could you help pls?

@zhougonglai
Copy link

i'm usage like:

import { style } from './dayCard.style';

interface Props {
  classes?: {
    container: string,
    pager: string,
    toolbox: string,
    animateWaper: string,
  };
}

@withStyles(style)
export default class Container extends React.Component<Props, object> {
...
render () {
    const { classes } = this.props;
    return (
      <div className={classes.container}>
        <QueueAnim className={classes.animateWaper} type="bottom">
          <Paper className={classes.pager} key="a">
            <Paper className={classes.toolbox} key="b">
....

Container.style.ts

import { StyleRules } from 'material-ui/styles';

const animateWaper = {
  display: 'flex',
  flex: 1,
};

export const style = (theme: any): StyleRules => ({
  container: {
    flex: 1,
    display: 'flex',
    padding: '3px 25px 0 0',
  },
  animateWaper,
  pager: {
    flex: 1,
  },
  toolbox: {
    height: '64px',
    display: 'flex',
    alignItems: 'center',
  },
});

Maybe not so good, but works!!

@NuclleaR
Copy link

@zhougonglai It doesn't work with v1.0.0-beta.16

@pelotom
Copy link
Member

pelotom commented Oct 15, 2017

I've opened #8694 which adds a TypeScript example project as well as some documentation on withStyles usage. In particular take a look at this file. I welcome any feedback on how to improve it!

@zhougonglai
Copy link

@pelotom this example is use 'single' class type. like WithStyles<'root'>,
but normally need a 'mulitple' class type. Maybe to write like WithStyles<'root' | 'container' | 'warper'>?

@gnapse
Copy link
Contributor

gnapse commented Jun 21, 2018

I wanted to ask about the recommended way to go forward using MenuItem's component with TypeScript. In particular regarding this feature of MenuItem:

Any other properties supplied will be spread to the root element

Consider this code:

import { Link } from 'react-router-dom';

/// ...

<MenuItem component={Link} to="/about">
  About page
</MenuItem>

It will complain about prop to not being expected by the MenuItem component. It is not technically for it, but it is used by it to pass it on to the root Link element. Works fine in js, but I wonder how to approach this to make TypeScript happy.

Update:

I ended up solving this, but with too much ceremony, so I'd still like to know if there's a more succinct way.

import { Link } from 'react-router-dom';
import MenuItem, { MenuItemProps } from '@material-ui/core/MenuItem';

interface LinkItemProps extends MenuItemProps {
  to?: string,
}

const LinkItem: React.ReactType<LinkItemProps> = MenuItem;

<LinkItem component={Link} to="/about">
  About page
</LinkItem>

@oliviertassinari
Copy link
Member

oliviertassinari commented Jun 21, 2018

@gnapse People have already been wondering about this point. I don't know any better answer than:

import { Link } from 'react-router-dom';

/// ...

<MenuItem component={props => <Link {...props} to="/about" />}>
  About page
</MenuItem>

@gnapse
Copy link
Contributor

gnapse commented Jun 21, 2018

Oh, so component does no need to be a component class, it can also be a render prop? Nice!

Update: @oliviertassinari I tried it and though it works in the sense that TypeScript does not complain anymore, my menu items with links do not work anymore :( Maybe it's a problem with my codebase, I'll try to investigate further to rule out it's the library.

@pelotom
Copy link
Member

pelotom commented Jun 21, 2018

It’s not treated as a render prop though, it’s treated as a (stateless functional) component. The implication of that is important to understand, because if you pass it a function inline like that, it will be treated as a brand new component on every render, which means that all components below Link will be unmounted and remounted with new instances on every render! If you don’t want that behavior you should declare your function at the module level so that it is stable.

@Defite
Copy link

Defite commented Jun 30, 2018

Well, it's not working.

<MenuItem component={ props => <Link {...props} to={ link.path } /> }>{ link.name }</MenuItem>

Gives error:

(25,54): Type '{ to: string; component?: string | ComponentClass<MenuItemProps> | StatelessComponent<MenuItemPro...' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<Link> & Readonly<{ children?: ReactNode; }> & Read...'.  Type '{ to: string; component?: string | ComponentClass<MenuItemProps> | StatelessComponent<MenuItemPro...' is not assignable to type 'Readonly<LinkProps>'.
    Types of property 'innerRef' are incompatible.
      Type 'string | ((instance: any) => any) | RefObject<any> | undefined' is not assignable to type '((node: HTMLAnchorElement | null) => void) | undefined'.
        Type 'string' is not assignable to type '((node: HTMLAnchorElement | null) => void) | undefined'.

I don't understand, why this issue was closed.

@boraturant
Copy link

boraturant commented Jul 10, 2018

@Defite @oliviertassinari Same here. Do we have any insight as to why below pattern does not work?

<MenuItem component={props => <Link {...props} to="/about" />}>

@Defite
Copy link

Defite commented Jul 10, 2018

@boraturant I think it is similar to this issue microsoft/TypeScript#16019 (comment)

@pelotom
Copy link
Member

pelotom commented Jul 10, 2018

There is a problem across the board with the typing of the overriding component prop in MUI components: it says that the overriding component receives the same props as the outer component, e.g.

export interface MenuItemProps extends StandardProps<ListItemProps, MenuItemClassKey> {
  component?: React.ReactType<MenuItemProps>;
  // ...
}

The particular issue you're having is that it thinks the innerRef prop added by withStyles could be passed to your inner component (even though it can't), and it happens that Link from react-router also has an innerRef prop, of a slightly different type. If you separate out the innerRef prop it should type check:

<MenuItem component={({ innerRef, ...props }) => <Link {...props} to="/about" />} />

There is a PR which would drastically improve the typing of components when overriding the component prop, allowing to write just

<MenuItem component={Link} to="/about" />

as we'd all prefer; unfortunately that PR has hit a bit of a snag in the type system...

@thril
Copy link

thril commented Jan 4, 2019

@gnapse Where does this Item come from?

const LinkItem: React.ReactType<LinkItemProps> = Item;

@gnapse
Copy link
Contributor

gnapse commented Jan 4, 2019

@thril sorry, it should be MenuItem instead. Here's the updated line.

const LinkItem: React.ReactType<LinkItemProps> = MenuItem;

I updated my comment accordingly. Thanks for pointing this out.

@charlax
Copy link
Contributor

charlax commented Apr 19, 2019

@gnapse your solution does not work for me:

import * as React from 'react';
import Button, { ButtonProps } from '@material-ui/core/Button';
import { Link } from 'react-router-dom';


interface LinkButtonProps extends ButtonProps {
  to?: string;
}

const LinkButton: React.ReactType<LinkButtonProps> = Button;

const C = () => {
  return (
      <div>
        <LinkButton
          variant="contained"
          color="primary"
          component={Link}
          to="/whatever"
        >
          Whatever
        </LinkButton>
      </div>
  );
};

export { C };

Still gives me:

TypeScript error: Type 'typeof Link' is not assignable to type '"object" | "label" | "style" | "title" | "button" | "time" | "link" | "menu" | "dialog" | "div" | "a" | "abbr" | "address" | "area" | "article" | "aside" | "audio" | "b" | "base" | ... 94 more ... | undefined'.
  Type 'typeof Link' is not assignable to type 'ComponentClass<ButtonProps, any>'.
    Types of parameters 'props' and 'props' are incompatible.
      Property 'to' is missing in type 'ButtonProps' but required in type 'Readonly<LinkProps>'.  TS2322

@GoelBiju

This comment has been minimized.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docs Improvements or additions to the documentation typescript
Projects
None yet
Development

No branches or pull requests