たとえば、以下のようなコンポーネントを考えます。
<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で注釈をつけるだけでうまく動きます。