A drawn image of Fredrik Bergqvist in a blue shirt

Fredrik Bergqvist

Web developer with over 22 years of experience. Writing about javascript and front end techniques

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;
  }
}

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"
  }
});

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"
  }
});

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;

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"
    }
  }
});

And use it in our app like this:

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

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.