Panda Noir

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

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

たとえば、以下のようなジェネリクスを使ったコンポーネントを考えます。

<Component<string> prop1="string" prop2="string" ref={ref} />

Component の prop1 と prop2 は同じ T 型とします(上の例では T は string)。

ref がなければカンタンに実装できる

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

type Props<T> = {
  prop1: T;
  prop2: T;
};
const Component = <T,>({ prop1, prop2 }: Props<T>) => (
  <ul>
    <li>prop1: {JSON.stringify(prop1)}</li>
    <li>prop2: {JSON.stringify(prop2)}</li>
  </ul>
);

<Component prop1="foo" prop2="bar" /> // 通る
<Component prop1="foo" prop2={42} /> // ちゃんと型エラーになる

codesandbox

シンプルですね。

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

forwardRef でラップするとジェネリクスが効かなくなります。

const ComponentWithRef = React.forwardRef(Component);

<ComponentWithRef prop1="foo" prop2={42} ref={ref}/>; // 型エラーになってくれない…

codesandbox

forwardRef が返すコンポーネントの型を指定する

これを回避するために、自分で forwardRef が返すコンポーネントの型を指定します。

type Props<T> = {
  prop1: T;
  prop2: T;
};
const Component = <T,>({ prop1, prop2 }: Props<T>) => (
  <ul>
    <li>prop1: {JSON.stringify(prop1)}</li>
    <li>prop2: {JSON.stringify(prop2)}</li>
  </ul>
);
const ComponentWithRef: <T,>(
  props: Props<T> & { ref: Ref<unknown> }
) => JSX.Element | null = forwardRef(Component);

codesandbox

おまけ: React.memo の場合

上の手法は React.memo でも使えますが、React.memo の場合は単に typeof を使う方がカンタンです。

const MemoizedComponent: typeof ComponentWithRef = React.memo(ComponentWithRef);

<MemoizedComponent prop1="foo" prop2={42} ref={ref} />; // きちんと型エラーになる

codesandbox

forwardRef と異なり React.memo でラップする前後で型は変わらないためうまく動きます。