Skip to content

MutableRefObject から LegacyRef への代入エラーを解決

問題概要

React で useRef フックを使用する際、特にコンポーネント内で ref 属性に渡そうとすると、以下の型エラーが発生することがあります:

typescript
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)

このエラーは主に次のようなコードの箇所で発生します:

tsx
const InputElement = React.forwardRef((props:any, ref) => {
    const handleRef = React.useRef<HTMLInputElement | undefined>(); // 初期値なし
    
    return <input ref={handleRef} type="checkbox" /> // ❌ エラーの発生箇所
});

エラーの原因

エラーメッセージが示す根本的な問題は、useRef の型定義と React 要素が期待する ref の型が一致していないことにあります:

  1. useRef を初期値なしで使用すると、current プロパティは undefined を許容する型になります

    • React.useRef<HTMLInputElement>()current: HTMLInputElement | undefined
  2. React の ref 属性は HTMLInputElement | null 型を期待しています

  3. undefinednull は TypeScript では互換性がない異なる型として扱われるため、型エラーになります

以下に型の互換性問題を図解します:

解決方法

正しい実装コード

エラーを修正するには、useRef の初期値として null を明示的に渡します:

tsx
const InputElement = React.forwardRef((props:any, ref) => {
    const handleRef = React.useRef<HTMLInputElement>(null); // null を初期値に設定
    
    return <input ref={handleRef} type="checkbox" />; // ✅ エラー解消
});

補足:React API 全体で統一した対応

この修正方法は他の状況でも有効です:

tsx
// 要素への直接参照の場合
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.currentnull になることに注意してください。DOM 操作を行う場合は useEffect 内で処理するか、値の存在チェックを行いましょう:

tsx
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<Tundefined>`
null`RefObject<Tnull>`

React の ref 設計の背景

React の ref システムが null を許容する設計になっている理由:

  1. アンマウント時のクリーンアップ: コンポーネントが DOM から削除された時、メモリリークを防ぐために ref.current 自動的に null に設定される
  2. ライフサイクルの整合性: マウント前/アンマウント後は ref オブジェクトが未接続状態となる
  3. 型安全性: null が明示的にチェックされている場合のみ DOM 操作を許可する保証

応用パターン

useImperativeHandle と併用する場合

問題文の例のように forwardRefuseImperativeHandle を組み合わせる場合でも、同じ解決法が適用できます:

tsx
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)と統合する場合は、双方向で型定義を調整:

tsx
// カスタムフック側の定義
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 の省略記法が利用できます:

tsx
// 明示的な型パラメータあり
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 の型安全性と完全互換性を保証します。