TypeScript

TypeScript

You can add static typing to JavaScript to improve developer productivity and code quality thanks to TypeScript. Have a look at the Create React App with TypeScript example. A minimum version of TypeScript 2.8 is required.

Usage of withStyles

Using withStyles in TypeScript can be a little tricky, but there are some utilities to make the experience as painless as possible.

Using createStyles to defeat type widening

A frequent source of confusion is TypeScript's type widening, which causes this example not to work as expected:

const styles = {
  root: {
    display: 'flex',
    flexDirection: 'column',
  }
};

withStyles(styles);
//         ^^^^^^
//         Types of property 'flexDirection' are incompatible.
//           Type 'string' is not assignable to type '"-moz-initial" | "inherit" | "initial" | "revert" | "unset" | "column" | "column-reverse" | "row"...'.

The problem is that the type of the flexDirection property is inferred as string, which is too arbitrary. To fix this, you can pass the styles object directly to withStyles:

withStyles({
  root: {
    display: 'flex',
    flexDirection: 'column',
  },
});

However type widening rears its ugly head once more if you try to make the styles depend on the theme:

withStyles(({ palette, spacing }) => ({
  root: {
    display: 'flex',
    flexDirection: 'column',
    padding: spacing.unit,
    backgroundColor: palette.background.default,
    color: palette.primary.main,
  },
}));

This is because TypeScript widens the return types of function expressions.

Because of this, we recommend using our createStyles helper function to construct your style rules object:

// Non-dependent styles
const styles = createStyles({
  root: {
    display: 'flex',
    flexDirection: 'column',
  },
});

// Theme-dependent styles
const styles = ({ palette, spacing }: Theme) => createStyles({
  root: {
    display: 'flex',
    flexDirection: 'column',
    padding: spacing.unit,
    backgroundColor: palette.background.default,
    color: palette.primary.main,
  },
});

createStyles is just the identity function; it doesn't "do anything" at runtime, just helps guide type inference at compile time.

Augmenting your props using WithStyles

Since a component decorated with withStyles(styles) gets a special classes prop injected, you will want to define its props accordingly:

const styles = (theme: Theme) => createStyles({
  root: { /* ... */ },
  paper: { /* ... */ },
  button: { /* ... */ },
});

interface Props {
  // non-style props
  foo: number;
  bar: boolean;
  // injected style props
  classes: {
    root: string;
    paper: string;
    button: string;
  };
}

However this isn't very DRY because it requires you to maintain the class names ('root', 'paper', 'button', ...) in two different places. We provide a type operator WithStyles to help with this, so that you can just write

import { WithStyles } from '@material-ui/core';

const styles = (theme: Theme) => createStyles({
  root: { /* ... */ },
  paper: { /* ... */ },
  button: { /* ... */ },
});

interface Props extends WithStyles<typeof styles> {
  foo: number;
  bar: boolean;
}

Decorating components

Applying withStyles(styles) as a function works as expected:

const DecoratedSFC = withStyles(styles)(({ text, type, color, classes }: Props) => (
  <Typography variant={type} color={color} classes={classes}>
    {text}
  </Typography>
));

const DecoratedClass = withStyles(styles)(
  class extends React.Component<Props> {
    render() {
      const { text, type, color, classes } = this.props
      return (
        <Typography variant={type} color={color} classes={classes}>
          {text}
        </Typography>
      );
    }
  }
);

Unfortunately due to a current limitation of TypeScript decorators, withStyles(styles) can't be used as a decorator in TypeScript.

Union props

When your props are a union, Typescript needs you to explicitly tell it the type, by providing a generic <Props> parameter to decorate:

interface Book {
  category: "book";
  author: string;
}

interface Painting {
  category: "painting";
  artist: string;
}

type BookOrPainting = Book | Painting;

type Props = BookOrPainting & WithStyles<typeof styles>;

const DecoratedUnionProps = withStyles(styles)<BookOrPainting>( // <-- without the type argument, we'd get a compiler error!
  class extends React.Component<Props> {
    render() {
      const props = this.props;
      return (
        <Typography classes={props.classes}>
          {props.category === "book" ? props.author : props.artist}
        </Typography>
      );
    }
  }
);

Customization of Theme

When adding custom properties to the Theme, you may continue to use it in a strongly typed way by exploiting Typescript's module augmentation.

The following example adds an appDrawer property that is merged into the one exported by material-ui:

import { Theme } from '@material-ui/core/styles/createMuiTheme';
import { Breakpoint } from '@material-ui/core/styles/createBreakpoints';

declare module '@material-ui/core/styles/createMuiTheme' {
  interface Theme {
    appDrawer: {
      width: React.CSSProperties['width']
      breakpoint: Breakpoint
    }
  }
  // allow configuration using `createMuiTheme`
  interface ThemeOptions {
    appDrawer?: {
      width?: React.CSSProperties['width']
      breakpoint?: Breakpoint
    }
  }
}

And a custom theme factory with additional defaulted options:

import createMuiTheme, { ThemeOptions } from '@material-ui/core/styles/createMuiTheme';

export default function createMyTheme(options: ThemeOptions) {
  return createMuiTheme({
    appDrawer: {
      width: 225,
      breakpoint: 'lg',
    },
    ...options,
  })
}

This could be used like:

import createMyTheme from './styles/createMyTheme';

const theme = createMyTheme({ appDrawer: { breakpoint: 'md' }});