DEV Community

Fredrik Bergqvist
Fredrik Bergqvist

Posted on • Originally published at bergqvist.it

Extending the theme in Material UI with TypeScript

When we started using Material UI (version 3) the support for extending the built-in theme was pretty lacking. The theme interface did not handle any additional color settings such as "success" or "warn" and trying to extend the theme did not work since parts of interfaces can't be overwritten.

So instead of extending the theme we used a separate object with corresponding interface to handle the extra colors that we needed. Not ideal but as the colors only were used in a few places we could afford to wait for the support in MUI to get better.

Flash forward a year and the support is here so extend the theme we did!
The documentation tells us to use module augmentation to merge our theme with the built-in theme by creating a index.d.ts file and adding our properties in that.

The official way of doing it

So if I want to extend the typography object to accept a secondaryFontFamily I would have to do something like this:

declare module "@material-ui/core/styles/createTypography" {
  interface TypographyOptions {
    secondaryFontFamily: string;
  }
  interface Typography {
    secondaryFontFamily: string;
  }
}
Enter fullscreen mode Exit fullscreen mode

And then creating a custom theme factory function to create the theme.

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

export const createMyTheme():CustomTheme => createMuiTheme({
  palette: createPalette({}),
  typography: {
    secondaryFontFamily: "Georgia"
  }
});
Enter fullscreen mode Exit fullscreen mode

This works well but still uses the Theme interface which makes it harder to know what has been extended.

Our project setup

We package our code in different NPM packages and use Lerna to handle the development environment.

That means that the theme is used over several packages and when we implemented the solution above we quickly realized that we had to add the index.d.ts file in every project, making it very cumbersome to add new attributes in the future.

Back to the drawing board.

A different solution

So we need an interface for our customised theme that we can share with our packages.

import React from "react";
export interface CustomTypography extends Typography {
  secondaryFontFamily: string;
}
export interface CustomTheme extends Theme {
  typography: CustomTypography;
}

export const createMyTheme():CustomTheme => createMuiTheme({
  palette: createPalette({}),
  typography: {
    secondaryFontFamily: "Georgia"
  }
});

Enter fullscreen mode Exit fullscreen mode

That will unfortunately result in the following error:
Image of code with a typescript error: S2345: Argument of type '{ palette: Palette; typography: { secondaryFontFamily: string; }; }' is not assignable to parameter of type 'ThemeOptions'. Types of property 'typography' are incompatible.

TypeScript does not allow interfaces to be merged and since CustomTheme extends Theme it seemed that we are out of luck.

Then I discovered Omit.

TypeScript Omit to the rescue!

Omit<T,K> is an utility type that constructs a type by picking all properties from T and then removing K.

So by using Omit we can create our own utility type Modify. (Props to Qwerty )

type Modify<T, R> = Omit<T, keyof R> & R;
Enter fullscreen mode Exit fullscreen mode

Which will merge two interfaces, removing any members on T that exists in R and then adding R to the resulting type.

So using Modify we can do this instead:

import { Theme } from "@material-ui/core";
import { Typography } from "@material-ui/core/styles/createTypography";

export type CustomTypography = Modify<
  Typography,
  {
    secondaryFontFamily: string;
  }
>;

export type CustomTheme = Modify<
  Theme,
  {
    typography: CustomTypography;
  }
>;


export const createMyTheme():CustomTheme => {
  const baseTheme = createMuiTheme({
    palette: createPalette({})
  });

  return {
    ...baseTheme,
    typography: {
      secondaryFontFamily: "Georgia"
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

And use it in our app like this:


const MyApp = () => {
const myTheme = createMyTheme();
  return (
    <ThemeProvider<CustomTheme> theme={myTheme}>
      <CssBaseline />
        <SomeComponent />
    </ThemeProvider>
  );
};

Enter fullscreen mode Exit fullscreen mode

I hope this can help get someone with the same problem some ideas and if you have solved the problem in another way, please let me know.

Oldest comments (2)

Collapse
 
codebelt profile image
Robert S. • Edited

Thanks Dude! This was great and it lead me to figure out this solution:

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

const theme = {
  typography: {
    secondaryFontFamily: 'Georgia',
  },
};

export type CustomTheme = Theme & typeof theme;

export const myTheme = createMuiTheme(
  {
    palette: createPalette({}),
  },
  theme
) as CustomTheme;

const MyApp: React.FC = () => {
  return (
    <ThemeProvider theme={myTheme}>
      <CssBaseline />
      <SomeComponent />
    </ThemeProvider>
  );
};
Enter fullscreen mode Exit fullscreen mode

Using Material-UI v4.11.1

Collapse
 
fredrikbergqvist profile image
Fredrik Bergqvist

Glad it could help!