styled-componentsは拡張元のコンポーネントに`theme`という名前のpropを渡さない

## 概要
styled-componentsは拡張元のコンポーネントに`theme`という名前のpropを渡さないようです。
そのため、拡張元のコンポーネントに`theme`という名前のpropを渡したい場合は少し工夫をする必要があります。

以下の説明で使用するサンプルコードは以下のURLに載せてあります。
https://github.com/ishioka0222/styled-components-theme-prop

## 経緯

[Ant Design](https://ant.design/)というReact用のUIフレームワークに、`theme`という名前のpropの値に応じて見た目を変更できるコンポーネントがあるのですが、そのコンポーネントをstyled-componentsで拡張したところ、`theme`という名前のpropの値だけが反映されなくなりました。
調べたところ、styled-componentsの実装に原因があることが分かったのですが、バグというよりは想定された挙動のようにも見えました。
備忘録として、ここに事象とその対処法について書き残しておきます。

## 動作を確認した環境

- node.js: v18.18.0
- react: v18.2.0
- react-dom: v18.2.0
- styled-components: v6.1.0

## 事象について

例えば、外部のライブラリに、`theme`というpropの値に応じて`filled`と`outlined`の2つの見た目を選べるような`ThirdPartyComponent`というコンポーネントがあるとします。

```jsx
type ThirdPartyTheme = "filled" | "outlined";

interface ThirdPartyComponentProps {
  className?: string;
  theme: ThirdPartyTheme;
}

const ThirdPartyComponent: React.FC<
  PropsWithChildren<ThirdPartyComponentProps>
> = (props) => {
  return (
    <div
      className={props.className}
      style={{
        padding: "0.5rem",
        margin: "0.5rem 0",
        border: "1px solid dodgerblue",
        backgroundColor:
          props.theme === "filled" ? "dodgerblue" : "transparent",
        color: props.theme === "filled" ? "whitesmoke" : "dodgerblue",
      }}
    >
      {`theme: ${props.theme}`}
      <hr />
      {props.children}
    </div>
  );
};

function App() {
  return (
    <>
      <ThirdPartyComponent theme={"filled"}>
        ThirdPartyComponent's children...
      </ThirdPartyComponent>
      <ThirdPartyComponent theme={"outlined"}>
        ThirdPartyComponent's children...
      </ThirdPartyComponent>
    </>
  );
}
```

このコンポーネントの`theme`の値に`filled`、`outlined`を指定すると、それぞれ以下のような見た目になります。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2585392/79324dc2-f528-fe8f-7eaf-be6f9f569973.png)

次に、このコンポーネントに影を付けようと思い、以下のようにstyled-componentsで拡張したとします。
```jsx
const StyledThirdPartyComponent = styled(ThirdPartyComponent)`
  box-shadow: 0 8px 8px 0px #333;
`;

function App() {
  return (
    <>
      <StyledThirdPartyComponent theme={"filled"}>
        StyledThirdPartyComponent's children...
      </StyledThirdPartyComponent>
      <StyledThirdPartyComponent theme={"outlined"}>
        StyledThirdPartyComponent's children...
      </StyledThirdPartyComponent>
    </>
  );
}
```
すると、`ThirdPartyComponent`が受け取った`theme`の値は`undefined`になってしまいます。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2585392/8a69d564-bfe8-9a06-8c83-4d7e4ea049d9.png)

こうなってしまう理由はよく分からないのですが、[styled-componentsのソースコードを見ると、以下の部分で`theme`というpropを意図的に読み飛ばしているようです](https://github.com/styled-components/styled-components/blob/669b67cfc85fb092fe26bf31492a3255e3af1145/packages/styled-components/src/models/StyledComponent.ts#L146)。
```jsx
if (context[key] === undefined) {
      // Omit undefined values from props passed to wrapped element.
      // This enables using .attrs() to remove props, for example.
    } else if (key[0] === '$' || key === 'as' || key === 'theme') {
      // Omit transient props and execution props.
    } else if (key === 'forwardedAs') {
      propsForElement.as = context.forwardedAs;
```
[styled-componentsにもtheme機能がある](https://styled-components.com/docs/advanced#theming)ため、それとの兼ね合いでしょうか・・・。

## 対処法について
[styled-componentsのGitHub Issuesでもこの件は報告されています](https://github.com/styled-components/styled-components/issues/4182)が、`theme`というpropの名前を変更して回避するしかないようです。

具体的には`theme`と被らないようなprop名を適当に用意します。ここでは仮に`_theme`とします[^1]。

[^1]: ここの変数名は基本的にはなんでも構いませんが、`$`から始まるpropはstyled-componentsで[transient prop](https://styled-components.com/docs/api#transient-props)と呼ばれ、拡張元のコンポーネントに渡されないため、一文字目には`$`以外の文字を使用する必要があります。

そして、styled-componentsでスタイルを拡張するときに、`_theme`で受け取った値を、`theme`という名前で拡張元のコンポーネントに渡すようにします。

```jsx
type StyledThirdPartyComponentProps = Omit<
  ThirdPartyComponentProps,
  "theme"
> & { _theme: ThirdPartyTheme };

const StyledThirdPartyComponent = styled(
  ({ _theme, ...props }: PropsWithChildren<StyledThirdPartyComponentProps>) => (
    <ThirdPartyComponent {...props} theme={_theme} />
  )
)`
  box-shadow: 0 8px 8px 0px #333;
`;

function App() {
  return (
    <>
      <StyledThirdPartyComponent _theme={"filled"}>
        StyledThirdPartyComponent's children...
      </StyledThirdPartyComponent>
      <StyledThirdPartyComponent _theme={"outlined"}>
        StyledThirdPartyComponent's children...
      </StyledThirdPartyComponent>
    </>
  );
}
```

こうすれば、以下の画像のように`_theme`の値が適用され、影も付けることができます。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2585392/f9f76b52-da84-58df-2829-ded5a87d136b.png)

上の説明で使用したサンプルコードは以下のURLに載せてあります。
https://github.com/ishioka0222/styled-components-theme-prop