第3回

useState・useRef・useReducerの型定義:TypeScriptで正しく型をつける

TypeScriptでReact HooksのuseState・useRef・useReducerに型をつける方法を解説。型推論が効くケース・明示が必要なケース・useRefのnull初期値問題・useReducerのaction型定義まで実例付きで説明します。

·10分で読める

useState の型定義

型推論が効くケース(型を書かなくていい)

初期値から型を推論できる場合は型引数を省略できます。

const [count, setCount] = useState(0)        // number と推論される
const [name, setName] = useState('')         // string と推論される
const [flag, setFlag] = useState(false)      // boolean と推論される
const [items, setItems] = useState<string[]>([])  // 空配列は明示が必要

型引数を明示すべきケース

// null や undefined になりうる場合
const [user, setUser] = useState<User | null>(null)

// 初期値が空配列・空オブジェクトの場合
const [tags, setTags] = useState<string[]>([])
const [errors, setErrors] = useState<Record<string, string>>({})

// ユニオン型
const [status, setStatus] = useState<'idle' | 'loading' | 'error'>('idle')

オブジェクトのstate

type FormState = {
  name: string
  email: string
  age: number
}

const [form, setForm] = useState<FormState>({
  name: '',
  email: '',
  age: 0,
})

// 部分的に更新するときはスプレッド構文
const handleNameChange = (name: string) => {
  setForm(prev => ({ ...prev, name }))
}

useRef の型定義

useRef は使い方によって型が変わるため注意が必要です。

DOM要素を参照する場合:初期値は null

import { useRef } from 'react'

const inputRef = useRef<HTMLInputElement>(null)

// アクセスするときはnullチェックが必要
const handleFocus = () => {
  inputRef.current?.focus()
}

<input ref={inputRef} />

useRef<HTMLInputElement>(null) と書くと RefObject<HTMLInputElement> 型になり、current は読み取り専用になります。これがDOM参照の正しい形です。

値を保持する場合:初期値はnull以外

// タイマーIDを保持する例
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)

const startTimer = () => {
  timerRef.current = setTimeout(() => {
    console.log('完了')
  }, 1000)
}

const stopTimer = () => {
  if (timerRef.current !== null) {
    clearTimeout(timerRef.current)
  }
}
// 前回の値を保持する例
const prevValueRef = useRef<string>('')  // null以外の初期値

useEffect(() => {
  prevValueRef.current = value  // 書き込み可能(MutableRefObject型)
}, [value])

初期値 null と null以外の違い

// null を渡す → RefObject(currentが読み取り専用)
const ref1 = useRef<HTMLInputElement>(null)
// ref1.current = something  // ❌ 型エラー

// null以外を渡す → MutableRefObject(currentが書き込み可能)
const ref2 = useRef<number>(0)
ref2.current = 42  // ✅ OK

DOM要素の参照には null を渡す、値の保持には初期値を渡すと覚えておけばOKです。

useReducer の型定義

状態管理が複雑になったときに useReducer を使います。

// Stateの型
type State = {
  count: number
  status: 'idle' | 'loading' | 'error'
  error: string | null
}

// Actionの型:Discriminated Union で定義する
type Action =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'reset' }
  | { type: 'setError'; payload: string }

// Reducer関数
const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + 1 }
    case 'decrement':
      return { ...state, count: state.count - 1 }
    case 'reset':
      return { count: 0, status: 'idle', error: null }
    case 'setError':
      return { ...state, status: 'error', error: action.payload }
    default:
      return state
  }
}

const initialState: State = { count: 0, status: 'idle', error: null }

// コンポーネントで使う
const Counter = () => {
  const [state, dispatch] = useReducer(reducer, initialState)

  return (
    <div>
      <p>{state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>リセット</button>
      {state.error && <p className="error">{state.error}</p>}
    </div>
  )
}

dispatch に渡す action の type が間違っていると型エラーになるため、バグを事前に防げます。

useCallback・useMemo の型

型推論が効くので、基本的に型引数を書く必要はありません。

// 戻り値の型は推論される
const handleClick = useCallback(() => {
  setCount(c => c + 1)
}, [])  // () => void と推論

const doubled = useMemo(() => count * 2, [count])  // number と推論

// 明示したい場合
const getValue = useCallback<() => number>(() => count, [count])

useContext の型

type Theme = 'light' | 'dark'

type ThemeContextType = {
  theme: Theme
  setTheme: (theme: Theme) => void
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined)

// カスタムフックで安全にアクセス
const useTheme = (): ThemeContextType => {
  const ctx = useContext(ThemeContext)
  if (ctx === undefined) {
    throw new Error('useTheme は ThemeProvider 内で使ってください')
  }
  return ctx
}

まとめ

Hook 型定義のポイント
useState 推論が効かない場合のみ <型> を明示。null になりうる場合は <T | null>
useRef(DOM) useRef<HTMLInputElement>(null) — 初期値は必ず null
useRef(値) useRef<number>(0) — null以外の初期値で書き込み可能に
useReducer ActionをDiscriminated Unionで定義すると型安全になる
useCallbackuseMemo 基本は型推論に任せる
useContext undefined を含むユニオン型+カスタムフックで安全に使う