TypeScript
You can add static typing to JavaScript to improve developer productivity and code quality thanks to TypeScript.
Material-UI requires a minimum version of TypeScript 3.2.
Have a look at the Create React App with TypeScript example.
In order for types to work, you have to at least have the following options enabled in your tsconfig.json
:
{
"compilerOptions": {
"lib": ["es6", "dom"],
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true
}
}
The strict mode options are the same that are required for every types package published in the @types/
namespace. Using a less strict tsconfig.json
or omitting some of the libraries might cause errors. To get the best type experience with the types we recommend setting "strict": true
.
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, using the createStyles
helper function to construct your style rules object is recommended:
// 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.
Media queries
withStyles
allows a styles object with top level media-queries like so:
const styles = createStyles({
root: {
minHeight: '100vh',
},
'@media (min-width: 960px)': {
root: {
display: 'flex',
},
},
});
However to allow these styles to pass TypeScript, the definitions have to be unambiguous concerning names for CSS classes and actual CSS property names. Due to this class names that are equal to CSS properties should be avoided.
// error because TypeScript thinks `@media (min-width: 960px)` is a class name
// and `content` is the css property
const ambiguousStyles = createStyles({
content: {
minHeight: '100vh',
},
'@media (min-width: 960px)': {
content: {
display: 'flex',
},
},
});
// works just fine
const ambiguousStyles = createStyles({
contentClass: {
minHeight: '100vh',
},
'@media (min-width: 960px)': {
contentClass: {
display: 'flex',
},
},
});
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, createStyles } 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.
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/createTheme';
import { Breakpoint } from '@material-ui/core/styles/createBreakpoints';
declare module '@material-ui/core/styles/createTheme' {
interface Theme {
appDrawer: {
width: React.CSSProperties['width']
breakpoint: Breakpoint
}
}
// allow configuration using `createTheme`
interface ThemeOptions {
appDrawer?: {
width?: React.CSSProperties['width']
breakpoint?: Breakpoint
}
}
}
And a custom theme factory with additional defaulted options:
./styles/createMyTheme:
import { createTheme, ThemeOptions } from '@material-ui/core/styles';
export default function createMyTheme(options: ThemeOptions) {
return createTheme({
appDrawer: {
width: 225,
breakpoint: 'lg',
},
...options,
})
}
This could be used like:
import createMyTheme from './styles/createMyTheme';
const theme = createMyTheme({ appDrawer: { breakpoint: 'md' }});
Usage of component
prop
Many Material-UI components allow you to replace their root node via a component
prop, this will be detailed in the component's API documentation. For example, a Button's root node can be replaced with a React Router's Link, and any additional props that are passed to Button, such as to
, will be spread to the Link component. For a code example concerning Button and react-router-dom checkout these demos.
To be able to use props of such a Material-UI component on their own, props should be used with type arguments. Otherwise, the component
prop will not be present in the props of the Material-UI component.
The examples below use TypographyProps
but the same will work for any component which has props defined with OverrideProps
.
The following CustomComponent
component has the same props as the Typography
component.
function CustomComponent(props: TypographyProps<'a', { component: 'a' }>) {
/* ... */
}
Now the CustomComponent
can be used with a component
prop which should be set to 'a'
. In addition, the CustomComponent
will have all props of a <a>
HTML element. The other props of the Typography
component will also be present in props of the CustomComponent
.
It is possible to have generic CustomComponent
which will accept any React component, custom and HTML elements.
function GenericCustomComponent<C extends React. ElementType>(
props: TypographyProps<C, { component?: C }>,
) {
/* ... */
}
Now if the GenericCustomComponent
will be used with a component
prop provided, it should also have all props required by the provided component.
function ThirdPartyComponent({ prop1 } : { prop1: string }) {
return <div />
}
// ...
function ThirdPartyComponent({ prop1 } : { prop1: string }) {
return <div />
}
// ...
The prop1
became required for the GenericCustomComponent
as the ThirdPartyComponent
has it as a requirement.
Not every component fully supports any component type you pass in. If you encounter a component that rejects its component
props in TypeScript please open an issue. There is an ongoing effort to fix this by making component props generic.
Handling value
and event handlers
Many components concerned with user input offer a value
prop or event handlers which include the current value
. In most situations that value
is only handled within React which allows it be of any type, such as objects or arrays.
However, that type cannot be verified at compile time in situations where it depends on the component's children e.g. for Select
or RadioGroup
. This means that the soundest option is to type it as unknown
and let the developer decide how they want to narrow that type down. We do not offer the possibility to use a generic type in those cases for the same reasons event.target
is not generic in React.
The demos include typed variants that use type casting. It is an acceptable tradeoff because the types are all located in a single file and are very basic. You have to decide for yourself if the same tradeoff is acceptable for you. The library types are be strict by default and loose via opt-in.