Material UI の PaletteColor の型をいい感じに拡張させる

半年前から React を触るようになり、Material UIも触るようになりました。
とても便利ですね！

UIフレームワークはこれまで Vue で Vuetify をつかってきましたが、断然 Material UI のほうが拡張性があり、使いやすくて結構好きになりました。

### ことの発端

デザイナーからの色指定で `primary` カラーのバリエーションに `light` `main` `dark` 以外の色を追加したいというユースケースが発生しました。仮に `disabled` とします。

具体的なコードを書くと、こんな感じにしたい。

```tsx
<Box bgcolor="primary.disabled">not available</Box>
```

JavaScript なら `createTheme` のところで下記のようにかけば良いのですが、 TypeScript の場合は `ThemeOptions` と型が合わずにエラーとなります。

```theme.ts
export const theme = createTheme({
  // 略
  palette: {
    primary: {
      light: '#ffe8d6',
      main: '#a5a58d',
      dark: '#6b705c',
      disabled: '#b7b7a4'
      // ↑ JS なら通るし Material UI の 色指定でアクセスできる
      // TS なら createTheme の時点でエラー。
      // 乱暴に @ts-ignore すればとりあえず大丈夫だが
      // theme を import したときにメンバーに出てこないのはイケてない
    }
  }
  // 略
});
```

上記のコメントでかいたとおり、 Material UI の theme 関係の恩恵が受けられませんが、メンバーを勝手に生やしてアクセスすること自体はできているにも関わらず、TSのコンパイルエラーになります。

なので、自前で型を変えちゃいます。

### `declare module` を使って上書き

Vue の 2系でよくやるやつですね（遠い目）。

いきなりですが、こんな感じの `d.ts` を書きました。

```types/createPalette.d.ts
import { Theme } from '@material-ui/core';
import { PaletteColor } from '@material-ui/core/styles/createPalette';

declare module '@material-ui/core/styles/createPalette' {
  interface CustomTheme extends Theme {
    palette: CustomPalette;
  }

  interface CustomPalette extends Palette {
    primary: CustomPaletteColor;
  }

  interface CustomPaletteColor extends PaletteColor {
    disabled?: string;
  }

  interface SimplePaletteColorOptions {
    disabled?: string;
  }
}
```

#### `SimplePaletteColorOptions`

ここで background を指定することで、 `createTheme` の palette.primary 内に `disabled`　が使えるようになりました。

#### `CustomPaletteColor`

一方、 `SimplePaletteColorOptions` だけを拡張しても、 theme を import してもメンバーに `disabled` が生えません。なので、 styled components で色を引っ張りたい場合に問題が出ます

```tsx
import React from 'react';
import { theme } from './theme.ts';

const hoge = (): JSX.Element => (
  <p style={{
    textDecoration: `underline overline ${theme.palette.primary.disabled}`
    //                                                          ~~~~~~~~ そんなものはない
  }}>hoge</p>
);

export default hoge;
```

なので、悩んだのですが、 既存の `PaletteColor` を拡張して使うことにします。

既に存在するメンバーを拡張させるのは `d.ts` を使ってもできないので、新しい interface を定義します。今回作成した `CustomPaletteColor` がそれです。

#### `CustomPalette`

上記と同じ理由です。今回は `primary` の PaletteColor だけを拡張させたいので、 `primary` だけ拡張した `CustomPaletteColor` を使いましたが、他の色 `error` とかも拡張させたい場合は、そちらも追記してください。

#### `CustomTheme`

これも上記と同じ & 本丸です。これを新しい　Theme の型として使いたいのですが、悩んだ結果、 `theme` に対して アサーションで読み替えてあげることにしました。

```ts
export const theme = createTheme({ /* 設定値 */ }) as CustomTheme;
```

### 実行結果

```tsx
import React from 'react';
import { theme } from './theme.ts';

const hoge = (): JSX.Element => (
  <p style={{
    textDecoration: `underline overline ${theme.palette.primary.disabled}`
    //                                                            ↑メンバーが生えた
  }}>hoge</p>
);

export default hoge;
```

これでいけそうです:tada:

もちろん、Material UIのコンポーネントでも指定できます。（こっちは文字列渡しなので型の恩恵は受けられませんが…）

```tsx
<Box bgcolor="primary.disabled">not available</Box>
```

### ところで

実は当初、型を眺めていたところ、色の指定で利用されている型 `PaletteColorOptions` では `ColorPartial` が union で指定されていて、普通に使えそうなんです。

```ts
// @material-ui/core/index.d.ts から抜粋
export interface Color {
  50: string;
  100: string;
  200: string;
  300: string;
  400: string;
  500: string;
  600: string;
  700: string;
  800: string;
  900: string;
  A100: string;
  A200: string;
  A400: string;
  A700: string;
}

// @material-ui/core/styles/createPalette.d.ts から抜粋
export type ColorPartial = Partial<Color>;

export type PaletteColorOptions = SimplePaletteColorOptions | ColorPartial;

export interface SimplePaletteColorOptions {
  light?: string;
  main: string;
  dark?: string;
  contrastText?: string;
}
```

ところが、実際に生える theme インスタンスの palette の中身の型は `PaletteColor` になっています。

```ts
// @material-ui/core/styles/createPalette.d.ts から抜粋
export interface PaletteColor {
  light: string;
  main: string;
  dark: string;
  contrastText: string;
}
```

同じ問題に気づいた方がいらっしゃたようで、こちらの Issue でディスカッションとなっていました。
https://github.com/mui-org/material-ui/issues/20277

で、このIssueを読んで、今回の対処法に行き着いた次第です。 

### まとめ

v5 ではもうちょっとかんたんに拡張できそうですね。

それでは。