MutableRefObject
から LegacyRef
への代入エラーを解決
問題概要
React で useRef
フックを使用する際、特にコンポーネント内で ref
属性に渡そうとすると、以下の型エラーが発生することがあります:
Type 'MutableRefObject<HTMLInputElement | undefined>' is not assignable to type 'LegacyRef<HTMLInputElement> | undefined'.
Type 'MutableRefObject<HTMLInputElement | undefined>' is not assignable to type 'RefObject<HTMLInputElement>'.
Types of property 'current' are incompatible.
Type 'HTMLInputElement | undefined' is not assignable to type 'HTMLInputElement | null'.
Type 'undefined' is not assignable to type 'HTMLInputElement | null'.ts(2322)
このエラーは主に次のようなコードの箇所で発生します:
const InputElement = React.forwardRef((props:any, ref) => {
const handleRef = React.useRef<HTMLInputElement | undefined>(); // 初期値なし
return <input ref={handleRef} type="checkbox" /> // ❌ エラーの発生箇所
});
エラーの原因
エラーメッセージが示す根本的な問題は、useRef
の型定義と React 要素が期待する ref
の型が一致していないことにあります:
useRef
を初期値なしで使用すると、current
プロパティはundefined
を許容する型になりますReact.useRef<HTMLInputElement>()
→current: HTMLInputElement | undefined
React の
ref
属性はHTMLInputElement | null
型を期待していますundefined
とnull
は TypeScript では互換性がない異なる型として扱われるため、型エラーになります
以下に型の互換性問題を図解します:
解決方法
正しい実装コード
エラーを修正するには、useRef
の初期値として null
を明示的に渡します:
const InputElement = React.forwardRef((props:any, ref) => {
const handleRef = React.useRef<HTMLInputElement>(null); // null を初期値に設定
return <input ref={handleRef} type="checkbox" />; // ✅ エラー解消
});
補足:React API 全体で統一した対応
この修正方法は他の状況でも有効です:
// 要素への直接参照の場合
const MyComponent = () => {
const inputRef = useRef<HTMLInputElement>(null);
return <input ref={inputRef} />;
};
// forwardRef を組み合わせる場合
const CustomInput = forwardRef<HTMLInputElement>((props, ref) => {
const internalRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => internalRef.current!);
return <input ref={internalRef} />;
});
// カスタムフックとの連携
const useCustomHook = () => {
const ref = useRef<HTMLElement>(null);
// ...ロジック
return ref;
};
重要な注意点
useRef
の初期値として null
を設定した場合、コンポーネントの初期レンダリング時には ref.current
が null
になることに注意してください。DOM 操作を行う場合は useEffect 内で処理するか、値の存在チェックを行いましょう:
useEffect(() => {
if (inputRef.current) {
// 安全にDOM操作が可能
inputRef.current.focus();
}
}, []);
解決策の技術的理由
型システムの動作原理
TypeScript が型の整合性をチェックする仕組みが原因でこのエラーが発生します:
useRef(null)
とすると型推論でRefObject<HTMLInputElement | null>
になる- React の
ref
属性が求める型RefObject<HTMLInputElement | null>
と一致 null
が代入可能な型となり、undefined
が排除されることで互換性が保たれる
型互換性の比較表
useRef 初期値 | 推論される型 | React の ref 互換性 |
---|---|---|
undefined | `RefObject<T | undefined>` |
null | `RefObject<T | null>` |
React の ref 設計の背景
React の ref システムが null
を許容する設計になっている理由:
- アンマウント時のクリーンアップ: コンポーネントが DOM から削除された時、メモリリークを防ぐために
ref.current
自動的にnull
に設定される - ライフサイクルの整合性: マウント前/アンマウント後は ref オブジェクトが未接続状態となる
- 型安全性:
null
が明示的にチェックされている場合のみ DOM 操作を許可する保証
応用パターン
useImperativeHandle と併用する場合
問題文の例のように forwardRef
と useImperativeHandle
を組み合わせる場合でも、同じ解決法が適用できます:
const InputElement = React.forwardRef((props:any, ref) => {
const handleRef = React.useRef<HTMLInputElement>(null); // null で初期化
React.useImperativeHandle(ref, () => ({
setChecked(checked:boolean) {
// null チェック後に操作
if (handleRef.current) {
handleRef.current.checked = checked;
}
}
}), []);
return <input ref={handleRef} type="checkbox" />;
});
カスタムフックとの統合
外部のカスタムフック(例:useOnClickOutside
)と統合する場合は、双方向で型定義を調整:
// カスタムフック側の定義
export function useOnClickOutside(
ref: React.MutableRefObject<HTMLElement | null>, // null を明示
handler: (event: MouseEvent) => void
): void {
// 実装...
}
// コンポーネント側の使用法
const MyComponent = () => {
const ref = useRef<HTMLDivElement>(null); // null で初期化
useOnClickOutside(ref, () => console.log('Clicked outside'));
return <div ref={ref}>...</div>;
};
最新ベストプラクティス
型安全性を高めるため、TypeScript 4.1 以降で追加された useRef
の省略記法が利用できます:
// 明示的な型パラメータあり
const ref = useRef<HTMLInputElement>(null);
// null初期化なら型引数推論可能
const ref = useRef<HTMLInputElement | null>(null);
まとめ
React と TypeScript を使用した開発で共通して発生するこのエラーは、useRef
の適切な初期化で解決できます:
- 💡 原因:
useRef
の初期値なし使用でundefined
が混入し型不一致 - ✅ 解決法:
useRef<ElementType>(null)
で明示的にnull
初期化 - 🛡️ 安全対策:
ref.current
操作時には常にnull
チェックを実行 - 🌐 互換性: DOM 参照・forwardRef・カスタムフックの全てのケースで対応可能
このシンプルな修正は、React の ref システムが意図的に設計された null
のライフサイクル管理に準拠し、TypeScript の型安全性と完全互換性を保証します。