Recently I ran into a problem with Material UI, styled-components, and TypeScript that I had a lot of trouble solving. Now that I have a working solution, I think its right to share what I did in case someone else runs into this problem.
The Scenario:
This is probably an uncommon scenario but probably not unique:
- The app is built mainly with React and Next.js, with most of the UI relying on Material UI (v4).
- We are using styled-components as a CSS-in-JS solution. I like this because it lets me use “real” css which I strongly prefer to the emotion style/style object syntax. Also, many of the components were built with styled-components before we adopted material UI, so it would be some effort to refactor that out.
- Since we introduced Material UI, we have been passing a single, unified theme object to both the styled-components theme provider and the material ui theme provider, which both wrap the entire app. This is done by passing the output of createTheme from material ui to both as the theme prop. This works quite nicely because it lets us use stuff like
theme.breakpoints.down('md')
in our styled-components CSS. - We have just introduced TypeScript. This is where things get challenging.
The Problem
Once we started converting files to TypeScript (and forgive me if anything comes across as ignorant here because I’m pretty new to TypeScript and strongly typed languages in general), we had to create a declaration for the DefaultTheme in styled-components. Since we are using the theme provided by createTheme from Material UI in our styled-components app, the situation is pretty messy. The type Theme
from material-ui is not compatible with the type DefaultTheme
from styled components.
The Solution
Ultimately, here’s how I fixed it.
1. Declare the styled-components DefaultTheme with my own types, filling in the missing types from material ui ThemeOptions to fill in missing values
// types/styled-components.d.ts
declare module 'styled-components' {
export interface DefaultTheme {
palette: palette;
overrides: overrides;
componentVariables: componentVariables;
breakpoints: breakpoints;
typography: typography;
spacing: ThemeOptions['spacing']; // this comes from Mui
props: ThemeOptions['props']; // this comes from Mui
mobileToDesktopBreakpoint: number;
}
}
(each of the properties in the DefaultTheme interface references a uniquely defined type but I left them out of the example for brevity)
2. Create two themes, one for MUI and one for SC from the same ThemeOptions root object
For the styled-components theme I cherry-picked properties from the Theme object from createTheme. It looks like this:
// theme.ts
const themeOptions = {
/**
this is where all the definitions for our theme go, such as custom palette colors, componentOverrides, etc
**/
}
const muiThemeOptions: ThemeOptions = {
...(themeOptions as ThemeOptions),
spacing: 8,
};
const muiTheme = createTheme(muiThemeOptions);
const scTheme: DefaultTheme = {
...(themeOptions as DefaultTheme), // spread the props from themeOptions
breakpoints: muiTheme.breakpoints, // add the properties from createTheme
spacing: muiTheme.spacing,
};
export {muiTheme, scTheme};
// _app.tsx
import {ThemeProvider as StyledComponentsThemeProvider} from 'styled-components';
import {ThemeProvider as MuiThemeProvider} from '@material-ui/core';
import {muiTheme, scTheme} from 'styles/theme/theme';
function App({Component}) {
return (
<MuiThemeProvider theme={muiTheme}>
<StyledComponentsThemeProvider theme={scTheme}>
<Component {...pageProps} />
</StyledComponentsThemeProvider>
</MuiThemeProvider>
);
}
3. Now we can use the mui theme values in our styled components
// SomeComponent.tsx
const StyledElement = styled.div`
${({theme}) => theme.breakpoints.down('md')} {
left: -200px;
}
`;
This solution came after some frustration around TypeScript telling me that I can’t use the Theme
type as the DefaultTheme
type that was expected to be passed to the styled-components theme provider. Since we had already merged themes before we were using TypeScript, in which case this was not an issue at all, my options were to figure out a way to merge the type definitions or refactor out all our uses of the MuiTheme properties in our styled-components. Our team finds it pretty useful to be able to use functions like theme.breakpoints.down()
and theme.spacing()
in our styled components, so this is definitely the solution we prefer for now.
Did this help you? Am I missing something or making a mistake in my understanding of types and TypeScript? Any other feedback? Let me know in the comments.