Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

React.forwardRef でジェネリクスを使いたい

たとえば、以下のようなコンポーネントを考えます。

<Hoge callback={callback} argument={argument} ref={ref}/>

Hogeコンポーネントは、argumentの型とcallbackの引数の型が一致してほしいです。

ref がない場合の実装

refさえなければ簡単に実装できます。

type FunctionType = (...args: any) => any;
type Props<T extends FunctionType> = {
  callback: T;
  argument: Parameters<T>;
};
const Hoge = <T extends FunctionType>({ callback, argument }: Props<T>) => (
  <div>
    Component using generics.
    <button onClick={() => callback(argument)}>click me</button>
  </div>
);

<Hoge
  callback={(n: number) => {
    console.log(n);
  }}
  argument={[3]}
/>;

このコンポーネントは、仮引数と実引数の型が異なるとエラーを吐いてくれます。型もかなり素直に書けています。

React.forwardRef が挟まるとジェネリクスが消える

ここまでは良いのですが、React.forwardRefが挟まると難しくなります。

const RefHoge = React.forwardRef(
  <T extends FunctionType>(
    { callback, argument }: Props<T>,
    ref: React.Ref<HTMLInputElement>
  ) => (
    <div>
      Component using generics.
      <button onClick={() => callback(argument)}>click me</button>
      <input ref={ref} />
    </div>
  )
);

<RefHoge
  callback={(n: number) => {
    console.log(n);
  }}
  argument={['11']}
/>;
// 仮引数と実引数の型が異なっているが型エラーが起きない

React.forwardRefでラップされているため、ジェネリクスがうまく働いていません。

React.FCを書きくだす

これを回避するために、React.FCを無理やり書き下します。もちろん、かなり邪道です。

type Component = (<T extends FunctionType>(
  props: Props<T>,
  ref: React.Ref<HTMLInputElement>
) => React.ReactElement | null) & { displayName?: string };

const RefHoge: Component = React.forwardRef(
  <T extends FunctionType>(
    { callback, argument }: Props<T>,
    ref: React.Ref<HTMLInputElement>
  ) => (
    <div>
      Component using generics.
      <button onClick={() => callback(argument)}>click me</button>
      <input ref={ref} />
    </div>
  )
);
<RefHoge
  callback={(n: number) => {
    console.log(n);
  }}
  argument={['11']}
/>;
// 型エラーが起きる

完全にはReact.FCと互換がとれておらず、問題が起きる可能性があります。使用する際は気をつけてください。

React.memoの場合

この手法はReact.memoでも使えます。しかし、React.memoの場合、もっと簡単な方法があります。

const MemoizedHoge = React.memo(RefHoge) as typeof RefHoge;

<MemoizedHoge
  callback={(n: number) => {
    console.log(n);
  }}
  argument={[false]}
/>;
// きちんと型エラーになる

基本的に型自体はReact.memoでラップする前後で変わらないので、asで注釈をつけるだけでうまく動きます。