Extending the theme in Material UI with TypeScript
Tags: typescript, materialui, react, javascript
Published: 2020-06-26 21:46:07

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](https://material-ui.com/guides/typescript/) tells us to use [module augmentation](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#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:

```typescript
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.

```typescript
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](https://github.com/lerna/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.

```typescript
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.](https://dev-to-uploads.s3.amazonaws.com/i/u44owg20izwd3tdgh732.PNG)

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>`](https://www.typescriptlang.org/docs/handbook/utility-types.html#omittk) 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](https://stackoverflow.com/questions/41285211/overriding-interface-property-type-defined-in-typescript-d-ts-file/55032655) )

```typescript
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:

```typescript
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:

```TypeScript

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.