中級11分で読める

【解決】TypeScript × React でよくある型エラー集:原因と修正パターン

TypeScript + Reactでよく遭遇する型エラーを解説します。propsの型定義・イベントハンドラの型・useStateの型引数・children・非同期の型推論など実務頻出パターンをまとめました。

TypeScriptを使い始めると、Reactが正常に動いていても型エラーが大量に出る。「これってなんで怒られているんだ」と思いながら as any を書いてしまいたくなるが、それは問題を先送りにしているだけだ。

型エラーは「TypeScriptがそのコードを理解できなかった」というサインだ。理由を知って正しい型で書き直す方が、長期的には時間がかからない。よくあるパターンを整理する。

イベントハンドラの型エラー

「eの型が推論されない」問題

// ❌ Parameter 'e' implicitly has an 'any' type
function Input() {
  const handleChange = (e) => {  // eの型が不明
    console.log(e.target.value)
  }
  return <input onChange={handleChange} />
}

handleChange を独立した変数として定義すると、TypeScriptはどんなイベントが渡ってくるかわからない。JSX上で onChange に直接書けば推論できるが、別変数のときは型を明示する。

// ✅ React.ChangeEvent<HTMLInputElement> を指定する
function Input() {
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value)
  }
  return <input onChange={handleChange} />
}

要素ごとに型が違うので、覚えておくと便利なものをまとめた。

// input, textarea の onChange
React.ChangeEvent<HTMLInputElement>
React.ChangeEvent<HTMLTextAreaElement>

// select の onChange
React.ChangeEvent<HTMLSelectElement>

// button の onClick
React.MouseEvent<HTMLButtonElement>

// form の onSubmit
React.FormEvent<HTMLFormElement>

// キーボードイベント
React.KeyboardEvent<HTMLInputElement>

useStateの型エラー

「nullで初期化したらその後setできない」問題

// ❌ null で初期化すると型が null に固定されてしまう
const [user, setUser] = useState(null)
// setUser(fetchedUser) → Argument of type 'User' is not assignable to 'null'

TypeScriptは初期値から型を推論する。初期値が null ならば型は null に確定し、あとで User 型の値を入れようとするとエラーになる。

// ✅ 型引数で「nullになりうるUser」と明示する
const [user, setUser] = useState<User | null>(null)

// 配列の場合も同じ
const [items, setItems] = useState<string[]>([])
const [errors, setErrors] = useState<Record<string, string>>({})

「TypeScriptが型を決めてくれるから型引数は不要」と思いがちだが、初期値が null[] の場合は明示が必要だ。

propsの型エラー

「必須のpropsが渡されていない」

type ButtonProps = {
  label: string
  onClick: () => void
}

// ❌ onClick が渡されていない
<Button label="送信" />
// Property 'onClick' is missing in type

型定義で ? のないプロパティは必須だ。省略できるようにするには ? を付ける。渡すことが前提なら呼び出し側を修正する。

// ✅ 省略可能にする
type ButtonProps = {
  label: string
  onClick?: () => void
}

// または必ず渡す
<Button label="送信" onClick={handleSubmit} />

「childrenの型が合わない」

// ❌ JSX.Element は1要素のみ。テキストや配列を受け取れない
type Props = {
  children: JSX.Element
}

JSX.Element は古い書き方で、1つのJSX要素しか受け取れない。実際には文字列・数値・配列・nullなど様々なものが children として渡ってくる。

// ✅ ReactNode はあらゆる型のchildrenを受け取れる
import type { ReactNode } from 'react'

type Props = {
  children: ReactNode
}

非同期関数の型エラー

「fetchの戻り値がanyになる」

// ❌ res.json() の戻り値は Promise<any>
async function fetchData() {
  const res = await fetch('/api/data')
  return res.json()  // any が返ってくる
}

fetch のAPIはJavaScriptの仕様で any を返す。TypeScriptはここで「何が来るかわからない」と判断して型のチェックを諦める。

// ✅ 期待するレスポンスの型を明示する
type ApiResponse = {
  users: User[]
  total: number
}

async function fetchData(): Promise<ApiResponse> {
  const res = await fetch('/api/data')
  if (!res.ok) throw new Error(`HTTP ${res.status}`)
  return res.json() as Promise<ApiResponse>
}

as Promise<ApiResponse> はキャストだが、fetchのAPIがanyを返す以上ここは例外的に使う。ただし型と実際のAPIレスポンスが一致していることを確認しておく必要がある。

配列のリテラル型が string になる問題

// ❌ 'all' | 'active' | 'completed' ではなく string[] に推論される
const FILTERS = ['all', 'active', 'completed']
type Filter = typeof FILTERS[number]  // string になってしまう

// ✅ as const でリテラル型として固定する
const FILTERS = ['all', 'active', 'completed'] as const
type Filter = typeof FILTERS[number]  // 'all' | 'active' | 'completed'

as const を付けると配列が読み取り専用になり、要素の型がリテラル型として固定される。フィルターの選択肢やステータス値など「有限の選択肢」を定義するときによく使う。

Discriminated Union(判別共用体)で状態を型安全に管理する

ローディング・成功・失敗という3つの状態をまとめて型で表現するパターン。

type Result<T> =
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; message: string }

status フィールドをキーにして、TypeScriptが各状態で存在するプロパティを絞り込んでくれる。

function UserCard({ result }: { result: Result<User> }) {
  switch (result.status) {
    case 'loading':
      return <div>読み込み中...</div>
    case 'success':
      return <div>{result.data.name}</div>  // ここでは result.data が必ず存在する
    case 'error':
      return <div>エラー: {result.message}</div>
  }
}

booleanisLoading isError を別々に持つより、状態の組み合わせが矛盾しない(isLoading=true かつ isError=true のような状態がない)というメリットがある。

forwardRefの型

DOM要素への参照を外部に公開したいとき、forwardRef の型引数は「DOM要素の型」と「propsの型」の2つを渡す。

import { forwardRef } from 'react'

type InputProps = {
  label: string
  placeholder?: string
}

// forwardRef<DOM要素の型, propsの型>
const LabeledInput = forwardRef<HTMLInputElement, InputProps>(
  ({ label, placeholder }, ref) => (
    <div>
      <label>{label}</label>
      <input ref={ref} placeholder={placeholder} />
    </div>
  )
)

型エラーと向き合うための考え方

TypeScriptのエラーは「このコードが正しいかどうかTypeScriptが判断できない」というメッセージだ。エラーを黙らせるために as any// @ts-ignore を使うのは、TypeScriptに「ここは見なくていい」と言っているのと同じで、型の恩恵を自分で壊している。

エラーが出たら「なぜTypeScriptはここで型を判断できないのか」を考えると、正しい修正が見えてくる。初期値がnullなら型引数で補う。推論できない場面では明示する。それだけで多くのエラーは解決する。