1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

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

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

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

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に型制約を入れるなら基本この形を使いましょう。

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

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

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でコンポーネントをラップしてみます。これだと一見エラーは発生しません。

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

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

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

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} />
)

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

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

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の構文としておかしくなっている、と言えます。

正しい例

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

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をつける

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

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

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

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?