Reactで型ジェネリクスを用いたコンポーネントを作る

型ジェネリクスを用いてコンポーネントを作る方法をまとめておく。もちろんFunctional Componentを前提とします。

# 型ジェネリクスを用いたReactコンポーネント

まずはいちばん簡単な例を示します。

```typescript
type Props<T> = {
  data: T
}

const Component = <T,>({ data }: Props<T>) => {
  return (
    <p>{`これは${typeof(data)}型のコンポーネントです。`}</p>
  )
}
```

ポイントとしては、関数の型ジェネリクスで`<T,>`とすることです。TypeScriptであれば関数の型ジェネリクスは通常`<T>() => {}`としますが、Reactコードの場合`<T>`はタグとみなされてしまうため、それとの区別のために`<T,>`とする必要があります。

もしくは、以下のような形でもOKです。`extends`を使えばコンパイラにタグではなく型ジェネリクスであるということを伝えられます。Tに型制約を入れるなら基本この形を使いましょう。

```typescript
const Component = <T extends {}>({ data }: Props<T>) => {
  // ...
}
```

このコンポーネントを使うときはこんな感じです。型引数を明示することもできます。

```typescript
const WrapperComponent: React.FC = () => (
  <Component data="文字列" />
)

//　これでもOK（型を明示する場合）
const WrapperComponent: React.FC = () => (
  <Component<string> data="文字列" />
)

// 明示した型とPropsがマッチしない場合はエラー
const WrapperComponent: React.FC = () => (
  <Component<number> data="文字列" /> // これはエラー、dataはnumber型
)
```

# 型ジェネリクスを用いたコンポーネントをメモ化する

## ダメな例

上記のコンポーネントをメモ化します。普段通り、`React.memo`でコンポーネントをラップしてみます。これだと一見エラーは発生しません。

```typescript
// エラーは発生しないので正しいコードのように見える
const Component = React.memo(<T extends {}>({ data }: Props<T>) => {
  // ...
})
Component.displayName = "Component"

const WrappedComponent: React.FC = () => (
  <Component data="文字列" />
)
```

しかし、この例には落とし穴があります。以下のようなコンポーネントを考えてみます。

```typescript
type Props<T> = {
  data: T
  arg: T
}

const Component = <T extends {}>({ data, arg }: Props<T>) => {
  return (
    <p>{`${typeof(data)} ${typeof(arg)}`}</p>
  )
}

const MemorizedComponent = React.memo(Component);
MemorizedComponent.displayName = "MemorizedComponent"

const WrappedComponent: React.FC = () => (
  // 型が異なるのにエラーが発生しない!
  <MemorizedComponent data="文字列" arg={42} />
)
```

`data`と`arg`の型が異なっているのにも関わらずエラーが発生しません。これは`React.memo`でラップされたことにより`MemorizedComponent`自体が`Props`に関して型ジェネリックになっていないためです。具体的には、`MemorizedComponent`の型は`React.MemoExoticComponent`型になっており、`Component`のPropsの型に関して無関心です。

:::note warn

メモ化する前と同じように呼び出し側で型引数を用いるとエラーになります。

```typescript
const Component = React.memo(<T extends {}>({ data }: Props<T>) => {
  // ...
})
Component.displayName = "Component"

// `<string>`部分でエラーが発生
// Expected 0 type arguments, but got 1.
const WrappedComponent: React.FC = () => (
  <Component<string> data="文字列" />
)
```

これもコンポーネントが`React.memo`でラップされたことに起因するものです。`React.memo`が型ジェネリクスを引数として期待していないため、そもそもTypeScriptの構文としておかしくなっている、と言えます。
:::

## 正しい例

メモ化したコンポーネントに型アサーションをすることでエラーを解消できます。型アサーションなのでもちろん実装する際は細心の注意を払う必要があります。

```typescript
type Props<T> = {
  data: T
  arg: T
}

const Component = <T extends {}>({ data, arg }: Props<T>) => {
  return (
    <p>{`${typeof(data)} ${typeof(arg)}`}</p>
  )
}

// Componentとして型アサーションをする
const MemorizedComponent = React.memo(Component) as typeof Component

// エラーが出る例
const WrappedComponent: React.FC = () => (
  // 今度はちゃんとエラーが出る！
  <MemorizedComponent data="文字列" arg={42} />
)

// 正しい例
const WrappedComponent: React.FC = () => (
  <MemorizedComponent data="文字列" arg="もじれつ" />
)
```

具体的には`Component`として型アサーションをするので、`MemorizedComponent`の型は`<T extends {}>({ data, arg }: Props<T>) => React.JSX.Element`となります。これによりメモ化以前と同様に型ジェネリクスを効かせることができます。

### displayNameをつける

上記の例では、`MemorizedComponent`に`displayName`プロパティをつけることができません。

```typescript
// Property 'displayName' does not exist
MemorizedComponent.displayName = "MemorizedComponent"
```

`Component`として型アサーションをしているので、基本上記のやり方でもESLintさんには怒られません。しかし、`displayName`を付けたい場合は以下のようにすればOKです。

```typescript
const MemorizedComponent = React.memo(Component) as typeof Component & { displayName: string }
MemorizedComponent.displayName = "MemorizedComponent"
```