React で TypeScript の型を書く場所は、大きく「props」「フック(useState・useRef など)」「イベントハンドラ」の3つです。 props は type で形を定義し、フックは初期値からの推論に任せつつ必要なところだけ型引数を付け、イベントは「イベント型+要素の型」の組み合わせで書きます。

この記事では、React 19.3 と @types/react 19.3 の組み合わせで、props・children・ref・useState・useRef・useReducer・イベントの型をコピペできる形でまとめました。掲載したコードはすべて tsc --noEmit で型チェックを通し、エラーの例は実際の tsc の出力をそのまま載せています。onSubmit の型が SubmitEvent になっているなど、現行の型定義に合わせた内容です。

確認環境:Ubuntu 24.04 / Node.js 22.22.2 / TypeScript 7.0.2 / react 19.3.0 / @types/react 19.3.0 / @types/react-dom 19.3.0(strict: true、jsx: react-jsx)。

React TypeScript 型の早見表

型を付けたいもの 書き方
props type Props = { label: string; size?: 'sm' | 'md' } を定義し、引数で ({ label, size }: Props)
children children: ReactNode
HTML 要素の属性をまとめて受ける ComponentProps<'button'> & { icon: string }
ref を props で受ける ref?: Ref<HTMLInputElement>
useState(単純な値) useState(0)(推論に任せる)
useState(null から始まる) useState<User | null>(null)
useState(空配列から始まる) useState<string[]>([])
useRef(DOM 要素) useRef<HTMLInputElement>(null)
useRef(値の入れ物) useRef<number | undefined>(undefined)
useReducer reducer の引数に (state: State, action: Action): State と書く
クリック (e: MouseEvent<HTMLButtonElement>) => void
入力欄の変更 (e: ChangeEvent<HTMLInputElement>) => void
フォーム送信 (e: SubmitEvent<HTMLFormElement>) => void
キー入力 (e: KeyboardEvent<HTMLInputElement>) => void
style 属性 style?: CSSProperties
setState を子へ渡す Dispatch<SetStateAction<number>>

型はすべて react から import します。 import type { ReactNode, MouseEvent } from 'react' のように、型だけを取り込むときは import type を使います。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「React TypeScriptの型の書き方|props・イベント型一覧」

準備:パッケージと tsconfig

npm i react react-dom
npm i -D typescript @types/react @types/react-dom
{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}
npx tsc -p .

React 公式の「Using TypeScript」のページには、tsconfig に必要な設定として 「lib に dom を含めること」と「jsx を有効な値にすること」 の2つが挙げられています。この2つが欠けると、後半の「設定まわりのエラー」に載せたメッセージが出ます。

props の型の書き方

次のファイルは、そのまま src/props.tsx として保存すれば型チェックが通ります。

import type { ComponentProps, CSSProperties, ReactNode, Ref } from 'react';

// 1. 基本の props:type で定義して、引数で分割代入する
type ButtonProps = {
  label: string;                       // 必須
  size?: 'sm' | 'md' | 'lg';           // 省略可(ユニオン型で選択肢を限定)
  disabled?: boolean;
  onPick: (id: number) => void;        // 関数
  style?: CSSProperties;               // style 属性
};
export function PickButton({ label, size = 'md', disabled = false, onPick, style }: ButtonProps) {
  return (
    <button className={size} disabled={disabled} style={style} onClick={() => onPick(1)}>
      {label}
    </button>
  );
}

// 2. children:ReactNode を使う
type CardProps = { title: string; children: ReactNode };
export function Card({ title, children }: CardProps) {
  return <section><h2>{title}</h2>{children}</section>;
}

// 3. HTML 要素の属性をそのまま受ける:ComponentProps<'button'> を広げる
type IconButtonProps = ComponentProps<'button'> & { icon: string };
export function IconButton({ icon, children, ...rest }: IconButtonProps) {
  return <button {...rest}><span>{icon}</span>{children}</button>;
}

// 4. ref を props で受ける(React 19):Ref<要素の型> を使う
type TextInputProps = { label: string; ref?: Ref<HTMLInputElement> };
export function TextInput({ label, ref }: TextInputProps) {
  return <label>{label}<input ref={ref} /></label>;
}

// 5. ジェネリックなコンポーネント
type ListProps<T> = { items: T[]; getKey: (item: T) => string; render: (item: T) => ReactNode };
export function List<T>({ items, getKey, render }: ListProps<T>) {
  return <ul>{items.map((it) => <li key={getKey(it)}>{render(it)}</li>)}</ul>;
}

// 使う側
export function Demo() {
  return (
    <Card title="備品">
      <PickButton label="選ぶ" size="lg" onPick={(id) => console.log(id)} />
      <IconButton icon="★" type="button" aria-label="お気に入り">保存</IconButton>
      <TextInput label="名前" />
      <List items={[{ id: 'a', name: 'ノート' }]} getKey={(i) => i.id} render={(i) => i.name} />
    </Card>
  );
}

props の書き方のポイント

書きたいこと 型 補足
必須の値 label: string 渡し忘れると TS2741 で知らせてくれる
省略できる値 size?: 'sm' | 'md' 引数側で size = 'md' と既定値を書く
決まった選択肢 'sm' | 'md' | 'lg' 文字列のユニオン型にすると補完が効く
関数 onPick: (id: number) => void 引数と戻り値を書く
子要素 children: ReactNode 文字列・数値・JSX・配列・null をすべて受けられる
ボタンの属性すべて ComponentProps<'button'> type・disabled・aria-label などをまとめて受ける
ref ref?: Ref<HTMLInputElement> React 19 ではふつうの props として受け取れる

React 19 では、関数コンポーネントが ref を props としてそのまま受け取れます。 上の TextInput に親から ref を渡して、親側で input の値を書き換えられることを jsdom 上で動かして確かめました(結果は ref を props で受けた input の値: set by parent)。

children の型:ReactNode・ReactElement・JSX.Element の違い

import type { JSX, ReactElement, ReactNode } from 'react';

const a: ReactNode = 'text';        // 文字列
const b: ReactNode = 42;            // 数値
const c: ReactNode = null;          // null / undefined / boolean
const d: ReactNode = <p>jsx</p>;    // JSX
const e: ReactNode = [a, b, d];     // 配列
const f: ReactElement = <p>jsx</p>; // JSX だけ
const g: JSX.Element = <p>jsx</p>;  // JSX だけ
export const all = [a, b, c, d, e, f, g];
型 受けられるもの 使いどころ
ReactNode JSX・文字列・数値・配列・null・undefined・boolean children はまずこれ
ReactElement JSX だけ 「必ず要素を1つ渡してほしい」とき
JSX.Element JSX だけ コンポーネントの戻り値に書くとき

ReactElement に文字列を入れると、次のエラーになります。

error TS2322: Type 'string' is not assignable to type 'ReactElement<unknown, string | JSXElementConstructor<any>>'.

フックの型の書き方

import { createContext, use, useContext, useEffect, useReducer, useRef, useState } from 'react';

type User = { id: number; name: string };

export function HooksDemo() {
  // --- useState ---
  const [count, setCount] = useState(0);                    // number と推論される
  const [user, setUser] = useState<User | null>(null);      // 最初は null、あとで User
  const [tags, setTags] = useState<string[]>([]);           // 空配列は型引数を付ける
  const [status, setStatus] = useState<'idle' | 'loading' | 'done'>('idle');

  // --- useRef ---
  const inputRef = useRef<HTMLInputElement>(null);          // DOM 要素:初期値は null
  const timerId = useRef<number | undefined>(undefined);    // 値の入れ物:初期値を必ず渡す
  const renders = useRef(0);                                // number と推論される

  useEffect(() => {
    inputRef.current?.focus();                              // null の可能性があるので ?. を使う
    timerId.current = window.setTimeout(() => setStatus('done'), 1000);
    return () => window.clearTimeout(timerId.current);
  }, []);
  renders.current += 1;

  return (
    <div>
      <input ref={inputRef} />
      <button onClick={() => { setCount((c) => c + 1); setUser({ id: 1, name: 'taro' }); setTags([...tags, 'new']); }}>
        {count} {user?.name ?? '未ログイン'} {tags.length} {status}
      </button>
    </div>
  );
}

// --- useReducer:state と action の型を reducer の引数に書く ---
type State = { items: string[]; filter: 'all' | 'done' };
type Action =
  | { type: 'added'; text: string }
  | { type: 'filtered'; filter: State['filter'] }
  | { type: 'cleared' };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'added': return { ...state, items: [...state.items, action.text] };
    case 'filtered': return { ...state, filter: action.filter };
    case 'cleared': return { ...state, items: [] };
  }
}
export function Todo() {
  const [state, dispatch] = useReducer(reducer, { items: [], filter: 'all' });
  return <button onClick={() => dispatch({ type: 'added', text: '発注' })}>{state.items.length}</button>;
}

// --- useContext:初期値 null のコンテキストは、取り出す所で null を取り除く ---
type Theme = { mode: 'light' | 'dark'; toggle: () => void };
const ThemeContext = createContext<Theme | null>(null);

export function useTheme(): Theme {
  const theme = useContext(ThemeContext);
  if (theme === null) throw new Error('ThemeContext の Provider の内側で使ってください');
  return theme;
}
export function ThemeLabel() {
  const { mode } = useTheme();              // ここでは Theme 型(null なし)
  const same = use(ThemeContext);           // use でも同じ型(Theme | null)が得られる
  return <span>{mode}{same?.mode}</span>;
}

useState の型:型引数が要る場面

初期値 書き方 理由
数値・文字列・真偽値 useState(0) 初期値から推論される
null から始まる useState<User | null>(null) そのままだと null 型に固定される
空配列から始まる useState<string[]>([]) そのままだと never[] になる
決まった文字列だけ useState<'idle' | 'done'>('idle') そのままだと string に広がる

useRef の型:初期値で使い分ける

@types/react 19.3 の useRef の定義は次の3つです(型定義ファイルから抜き出したもの)。

function useRef<T>(initialValue: T): RefObject<T>;
function useRef<T>(initialValue: T | null): RefObject<T | null>;
function useRef<T>(initialValue: T | undefined): RefObject<T | undefined>;
用途 書き方 current の型
DOM 要素を参照する useRef<HTMLInputElement>(null) HTMLInputElement | null
あとで入れる値 useRef<number | undefined>(undefined) number | undefined
初期値がある値 useRef(0) number

React 19 の型では、useRef に引数が必須です。 戻り値はどの書き方でも RefObject で、current へ代入できます。

useReducer の型:reducer の引数に書く

useReducer には型引数を付けず、reducer 関数の引数と戻り値に型を書く のが現行の書き方です。dispatch に渡せる action は、reducer の Action 型から自動で決まります。React 19 のアップグレードガイドにも「useReducer には Reducer 型を型引数として渡さず、文脈からの推論に任せる」という方針が書かれています。

そのほかのよく使う型

import { useCallback, useMemo, useState } from 'react';
import type { ComponentProps, ComponentRef, Dispatch, SetStateAction } from 'react';

// 子に setState をそのまま渡すときの型
type CounterProps = { count: number; setCount: Dispatch<SetStateAction<number>> };
export function CounterButton({ count, setCount }: CounterProps) {
  return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
}
// 既存コンポーネントの props 型を取り出す
type P = ComponentProps<typeof CounterButton>;
// 要素やコンポーネントの ref の型を取り出す
type InputEl = ComponentRef<'input'>; // HTMLInputElement

export function Parent() {
  const [count, setCount] = useState(0);
  const doubled = useMemo(() => count * 2, [count]);            // number と推論
  const onPick = useCallback((id: number) => console.log(id), []); // 引数に型を書く
  const props: P = { count, setCount };
  const el: InputEl | null = null;
  return <p onClick={() => onPick(doubled)}><CounterButton {...props} />{String(el)}</p>;
}

イベントの型一覧

イベントハンドラの引数は 「イベント型」に「ハンドラを付けた要素の型」を型引数として渡す 形で書きます。次の表は、@types/react 19.3.0 の型定義ファイルで各属性の定義を確かめてまとめたものです。

属性 イベント型 ハンドラ全体の型 主な用途
onClick・onDoubleClick・onContextMenu MouseEvent MouseEventHandler クリック
onMouseEnter・onMouseMove MouseEvent MouseEventHandler マウスの出入り・移動
onChange ChangeEvent ChangeEventHandler input・select・textarea の変更
onInput・onBeforeInput InputEvent InputEventHandler 入力のたびに
onSubmit SubmitEvent SubmitEventHandler フォーム送信
onKeyDown・onKeyUp KeyboardEvent KeyboardEventHandler キー入力
onFocus・onBlur FocusEvent FocusEventHandler フォーカスの出入り
onCopy・onCut・onPaste ClipboardEvent ClipboardEventHandler コピー・貼り付け
onDrag・onDrop DragEvent DragEventHandler ドラッグ&ドロップ
onPointerDown・onPointerMove PointerEvent PointerEventHandler マウス・タッチ・ペン共通
onTouchStart・onTouchMove TouchEvent TouchEventHandler タッチ操作
onScroll UIEvent UIEventHandler スクロール
onWheel WheelEvent WheelEventHandler ホイール
onCompositionStart・onCompositionEnd CompositionEvent CompositionEventHandler 日本語入力の変換
onAnimationStart・onAnimationEnd AnimationEvent AnimationEventHandler CSS アニメーション
onTransitionEnd TransitionEvent TransitionEventHandler CSS トランジション
onToggle ToggleEvent ToggleEventHandler details・popover の開閉
onLoad・onError・onSelect・onReset・onInvalid SyntheticEvent ReactEventHandler 読み込み完了など

onSubmit は SubmitEvent、onInput は InputEvent です。 現行の型定義では FormEvent と FormEventHandler に非推奨の印(deprecated)が付いています。FormEvent を使ったコードも型チェックは通りますが、新しく書くなら SubmitEvent を選ぶとよいでしょう。

要素の型一覧

イベント型や useRef の型引数に渡す、要素ごとの型です(すべて useRef に渡して型チェックを通しています)。

タグ 要素の型 タグ 要素の型
button HTMLButtonElement input HTMLInputElement
select HTMLSelectElement textarea HTMLTextAreaElement
form HTMLFormElement label HTMLLabelElement
div HTMLDivElement span HTMLSpanElement
a HTMLAnchorElement img HTMLImageElement
p HTMLParagraphElement h1〜h6 HTMLHeadingElement
ul HTMLUListElement li HTMLLIElement
table HTMLTableElement dialog HTMLDialogElement
canvas HTMLCanvasElement video HTMLVideoElement
svg SVGSVGElement

イベントハンドラのコピペ用コード

import type {
  ChangeEvent, ClipboardEvent, DragEvent, FocusEvent, InputEvent, KeyboardEvent,
  MouseEvent, MouseEventHandler, PointerEvent, SubmitEvent, UIEvent, WheelEvent,
} from 'react';
import { useState } from 'react';

export function EventsDemo() {
  const [text, setText] = useState('');

  // 関数を先に定義する場合は、引数に「イベント型<要素の型>」を書く
  function handleClick(e: MouseEvent<HTMLButtonElement>) {
    console.log(e.currentTarget.name, e.clientX);
  }
  function handleChange(e: ChangeEvent<HTMLInputElement>) {
    setText(e.target.value);
  }
  function handleSelect(e: ChangeEvent<HTMLSelectElement>) {
    console.log(e.target.value);
  }
  function handleTextarea(e: ChangeEvent<HTMLTextAreaElement>) {
    console.log(e.target.value.length);
  }
  function handleSubmit(e: SubmitEvent<HTMLFormElement>) {
    e.preventDefault();
    const data = new FormData(e.currentTarget);
    console.log(data.get('q'));
  }
  function handleKeyDown(e: KeyboardEvent<HTMLInputElement>) {
    if (e.key === 'Enter' && !e.nativeEvent.isComposing) console.log('確定');
  }
  function handleFocus(e: FocusEvent<HTMLInputElement>) {
    e.target.select();
  }
  function handleInput(e: InputEvent<HTMLInputElement>) {
    console.log(e.data);
  }
  function handlePaste(e: ClipboardEvent<HTMLInputElement>) {
    console.log(e.clipboardData.getData('text'));
  }
  function handleDrop(e: DragEvent<HTMLDivElement>) {
    e.preventDefault();
    console.log(e.dataTransfer.files.length);
  }
  function handlePointer(e: PointerEvent<HTMLDivElement>) {
    console.log(e.pointerType, e.pressure);
  }
  function handleScroll(e: UIEvent<HTMLDivElement>) {
    console.log(e.currentTarget.scrollTop);
  }
  function handleWheel(e: WheelEvent<HTMLDivElement>) {
    console.log(e.deltaY);
  }
  // 関数全体に型を付ける書き方(引数の型は推論される)
  const handleEnter: MouseEventHandler<HTMLDivElement> = (e) => {
    console.log(e.currentTarget.id);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="q" value={text} onChange={handleChange} onKeyDown={handleKeyDown}
        onFocus={handleFocus} onBlur={(e) => console.log(e.target.value)}
        onInput={handleInput} onPaste={handlePaste} />
      <select onChange={handleSelect}><option>A</option></select>
      <textarea onChange={handleTextarea} />
      <div id="area" onDrop={handleDrop} onDragOver={(e) => e.preventDefault()} onPointerDown={handlePointer}
        onScroll={handleScroll} onWheel={handleWheel} onMouseEnter={handleEnter} />
      {/* インラインで書けば e の型は自動で決まる */}
      <button name="send" onClick={handleClick}>送信</button>
      <button type="button" onClick={(e) => console.log(e.currentTarget.name)}>別の操作</button>
    </form>
  );
}

イベントの型を書くときの3つのコツ

コツ 内容
インラインなら型は不要 onClick={(e) => ...} と属性に直接書けば、e の型は自動で決まる
型がわからないときはインラインで書いてみる エディタで e にカーソルを当てると型が表示されるので、それを写す
currentTarget と target を使い分ける currentTarget は「ハンドラを付けた要素」で型が付く。target は「実際に操作された要素」で、ChangeEvent と FocusEvent 以外では EventTarget 型になる

tsc のエラー文と直し方

ここからは、実際に tsc が出したエラー文と、直したコードをセットで載せます。直したコードは、先頭に次の import を置いた1つのファイルにまとめて型チェックを通したものです。

import { createContext, useContext, useReducer, useRef, useState } from 'react';
import type { ChangeEvent, JSX, MouseEvent, ReactElement, ReactNode } from 'react';
エラー文(要点) コード 原因 直し方
Expected 1 arguments, but got 0. TS2554 useRef() を引数なしで呼んだ 初期値(null や undefined)を渡す
Cannot find namespace 'JSX'. TS2503 グローバルの JSX を参照した import type { JSX } from 'react'
'inputRef.current' is possibly 'null'. TS18047 ref の null を考慮していない ?. を使う、または if で確かめる
...'id' does not exist in type '(prevState: null) => null'. TS2353 useState(null) で型が null に固定 useState<User | null>(null)
Type 'string' is not assignable to type 'never'. TS2322 useState([]) が never[] になった useState<string[]>([])
Property 'name' does not exist on type 'EventTarget'. TS2339 e.target を使った e.currentTarget を使う
Parameter 'e' implicitly has an 'any' type. TS7006 ハンドラの引数に型がない 引数にイベント型を書く
Property 'children' does not exist on type 'IntrinsicAttributes & CardProps'. TS2322 props に children がない children: ReactNode を足す
Property 'count' is missing in type ... but required in type 'CardProps'. TS2741 必須の props を渡していない 渡すか、count?: にする
Type 'HTMLSelectElement' is missing the following properties from type 'HTMLInputElement' TS2322 要素とイベント型の組み合わせが違う 要素に合った型にする
'theme' is possibly 'null'. TS18047 null 入りのコンテキストをそのまま使った null を取り除くカスタムフックを通す
'icon.props' is of type 'unknown'. TS18046 ReactElement の props が unknown ReactElement<{ title: string }>
Type 'null' is not assignable to type 'void | (() => VoidOrUndefinedOnly)'. TS2322 ref コールバックが値を返している 波かっこで囲んで値を返さない
Expected 2 type arguments, but got 1. TS2558 useReducer<Reducer<S, A>> と書いた 型引数を外す
Type 'string' is not assignable to type 'number'. TS2322 props の型と違う値を渡した count={3} のように型を合わせる

useRef に引数が必要(TS2554)

// エラーになる例
import { useRef } from 'react';
export function A() {
  const timer = useRef<number>();
  return <p>{timer.current}</p>;
}
err/e01_useref_noarg.tsx(3,17): error TS2554: Expected 1 arguments, but got 0.
// 直したコード
export function F01() {
  const timer = useRef<number | undefined>(undefined);
  return <p>{timer.current}</p>;
}

React 19 のアップグレードガイドには「useRef は引数が必須になった」と書かれています。以前の書き方のコードを React 19 の型へ上げると、まずこのエラーに出会います。

JSX 名前空間が見つからない(TS2503)

// エラーになる例
export function A(): JSX.Element {
  return <p>hello</p>;
}
err/e02_jsx_namespace.tsx(1,22): error TS2503: Cannot find namespace 'JSX'.
// 直したコード
import type { JSX } from 'react';
export function F02(): JSX.Element {
  return <p>hello</p>;
}

同じアップグレードガイドによると、React 19 の型ではグローバルの JSX 名前空間がなくなり、React.JSX(react から import する JSX)を使う形になりました。戻り値の型を書かずに推論へ任せる方法でも解消できます。

null の可能性がある(TS18047)

// エラーになる例
import { useRef } from 'react';
export function A() {
  const inputRef = useRef<HTMLInputElement>(null);
  return <button onClick={() => inputRef.current.focus()}>focus<input ref={inputRef} /></button>;
}
err/e03_ref_null.tsx(4,33): error TS18047: 'inputRef.current' is possibly 'null'.
// 直したコード
export function F03() {
  const inputRef = useRef<HTMLInputElement>(null);
  return <button onClick={() => inputRef.current?.focus()}>focus<input ref={inputRef} /></button>;
}

ref は要素が画面に出るまで null なので、?. で「あれば実行」と書きます。コンテキストでも同じエラーが出ます。

err/e11_context_null.tsx(5,14): error TS18047: 'theme' is possibly 'null'.

こちらは「フックの型の書き方」に載せた useTheme のように、null を取り除くカスタムフックを1つ用意する と、使う側で毎回 null を確かめずに済みます。

useState の型が狭すぎる(TS2353・TS2322)

// エラーになる例(2つのファイル)
import { useState } from 'react';
export function A() {
  const [user, setUser] = useState(null);
  return <button onClick={() => setUser({ id: 1, name: 'taro' })}>{String(user)}</button>;
}

import { useState } from 'react';
export function A() {
  const [tags, setTags] = useState([]);
  return <button onClick={() => setTags([...tags, 'new'])}>{tags.length}</button>;
}
err/e04_usestate_null.tsx(4,43): error TS2353: Object literal may only specify known properties, and 'id' does not exist in type '(prevState: null) => null'.
err/e05_usestate_empty_array.tsx(4,42): error TS2322: Type 'string' is not assignable to type 'never'.
// 直したコード
type User = { id: number; name: string };
export function F04() {
  const [user, setUser] = useState<User | null>(null);
  return <button onClick={() => setUser({ id: 1, name: 'taro' })}>{user?.name}</button>;
}
export function F05() {
  const [tags, setTags] = useState<string[]>([]);
  return <button onClick={() => setTags([...tags, 'new'])}>{tags.length}</button>;
}

e.target にプロパティがない(TS2339)と引数の型がない(TS7006)

// エラーになる例(2つのファイル)
import type { MouseEvent } from 'react';
export function A() {
  function handleClick(e: MouseEvent<HTMLButtonElement>) {
    console.log(e.target.name);
  }
  return <button name="send" onClick={handleClick}>送信</button>;
}

export function A() {
  function handleChange(e) {
    console.log(e.target.value);
  }
  return <input onChange={handleChange} />;
}
err/e06_event_target.tsx(4,26): error TS2339: Property 'name' does not exist on type 'EventTarget'.
err/e07_implicit_any.tsx(2,25): error TS7006: Parameter 'e' implicitly has an 'any' type.
// 直したコード
export function F06() {
  function handleClick(e: MouseEvent<HTMLButtonElement>) {
    console.log(e.currentTarget.name);
  }
  return <button name="send" onClick={handleClick}>送信</button>;
}
export function F07() {
  function handleChange(e: ChangeEvent<HTMLInputElement>) {
    console.log(e.target.value);
  }
  return <input onChange={handleChange} />;
}

要素とイベント型の組み合わせが違う(TS2322)

select に ChangeEvent<HTMLInputElement> のハンドラを渡したときの出力です。

err/e10_wrong_element.tsx(6,18): error TS2322: Type '(e: ChangeEvent<HTMLInputElement, Element>) => void' is not assignable to type 'ChangeEventHandler<HTMLSelectElement, HTMLSelectElement>'.
  Types of parameters 'e' and 'event' are incompatible.
    Type 'ChangeEvent<HTMLSelectElement, HTMLSelectElement>' is not assignable to type 'ChangeEvent<HTMLInputElement, Element>'.
      Type 'HTMLSelectElement' is missing the following properties from type 'HTMLInputElement': accept, align, alt, capture, and 38 more.

長いメッセージですが、最後の行に「HTMLSelectElement には HTMLInputElement のプロパティが足りない」と理由が書かれています。 型引数を HTMLSelectElement に直せば通ります。

children が props にない(TS2322)

// エラーになる例
type CardProps = { title: string };
function Card({ title }: CardProps) {
  return <h2>{title}</h2>;
}
export function A() {
  return <Card title="備品"><p>本文</p></Card>;
}
err/e08_children.tsx(6,11): error TS2322: Type '{ title: string; children: Element; }' is not assignable to type 'IntrinsicAttributes & CardProps'.
  Property 'children' does not exist on type 'IntrinsicAttributes & CardProps'.
// 直したコード
type CardProps = { title: string; children?: ReactNode };
function Card({ title, children }: CardProps) {
  return <section><h2>{title}</h2>{children}</section>;
}
export function F08() {
  return <Card title="備品"><p>本文</p></Card>;
}

FC 型を使う書き方でも、children は自動では入りません。FC<{ title: string }> のコンポーネントで children を取り出すと、次のエラーになります。

err/e17_fc_children.tsx(2,48): error TS2339: Property 'children' does not exist on type '{ title: string; }'.

children を受けるコンポーネントは、どの書き方でも children: ReactNode を自分で書く。 これが現行の型での決まりです。

React 19 の型で出やすいそのほかのエラー

// エラーになる例(3つのファイル)
import type { ReactElement } from 'react';
export function A({ icon }: { icon: ReactElement }) {
  return <p>{icon.props.title}</p>;
}

export function A() {
  let node: HTMLDivElement | null = null;
  return <div ref={(el) => (node = el)}>{String(node)}</div>;
}

import { useReducer, type Reducer } from 'react';
type State = { n: number };
type Action = { type: 'inc' };
const reducer = (s: State, a: Action): State => (a.type === 'inc' ? { n: s.n + 1 } : s);
export function A() {
  const [state] = useReducer<Reducer<State, Action>>(reducer, { n: 0 });
  return <p>{state.n}</p>;
}
err/e12_element_props.tsx(3,14): error TS18046: 'icon.props' is of type 'unknown'.
err/e13_ref_callback_return.tsx(3,15): error TS2322: Type '(el: HTMLDivElement | null) => HTMLDivElement | null' is not assignable to type 'Ref<HTMLDivElement> | undefined'.
err/e14_usereducer_generic.tsx(6,30): error TS2558: Expected 2 type arguments, but got 1.
// 直したコード
export function F12({ icon }: { icon: ReactElement<{ title: string }> }) {
  return <p>{icon.props.title}</p>;
}
export function F13() {
  const nodeRef = useRef<HTMLDivElement | null>(null);
  return <div ref={(el) => { nodeRef.current = el; }}>box</div>;
}
type State = { n: number };
type Action = { type: 'inc' };
const reducer = (s: State, a: Action): State => (a.type === 'inc' ? { n: s.n + 1 } : s);
export function F14() {
  const [state] = useReducer(reducer, { n: 0 });
  return <p>{state.n}</p>;
}

3つとも React 19 のアップグレードガイドの「TypeScript changes」に載っている変更です。同じガイドには、こうした書き換えをまとめて行うコマンドとして npx types-react-codemod@latest preset-19 ./path-to-app が紹介されています。

設定まわりのエラー

エラー文 コード 原因 直し方
Cannot use JSX unless the '--jsx' flag is provided. TS17004 tsconfig に jsx がない "jsx": "react-jsx" を足す
This JSX tag requires 'React' to be in scope, but it could not be found. TS2874 "jsx": "react" で React を import していない "jsx": "react-jsx" にする
Cannot find module 'react' or its corresponding type declarations. TS2307 @types/react が入っていない npm i -D @types/react @types/react-dom
This JSX tag requires the module path 'react/jsx-runtime' to exist, but none could be found. TS2875 同上 同上
JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists. TS7026 同上 同上

よくある質問

Q. props は type と interface のどちらで書けばよいですか? A. どちらでも型チェックの結果は同じです。この記事では、ユニオン型や ComponentProps<'button'> & {...} のような組み合わせをそのまま書ける type でそろえました。チームで決まっている書き方があれば、それに合わせるのがいちばんです。

Q. コンポーネントの戻り値の型は書いたほうがよいですか? A. 書かなくても推論されます。書く場合は import type { JSX } from 'react' をしたうえで JSX.Element とします。

Q. React.FC は使ってもよいですか? A. 使えます。ただし children は自動では入らないので、受け取るなら children?: ReactNode を props の型に書きます。関数の引数に props の型を直接書く方法なら、ジェネリックなコンポーネントも同じ書き方で作れます。

Q. イベントの型が思い出せないときはどうすればよいですか? A. いったん onClick={(e) => ...} のようにインラインで書き、エディタで e の型を見て写すのが手早い方法です。この記事の「イベントの型一覧」も使ってください。

まとめ

  • props は type で定義し、引数で分割代入する。 children は ReactNode、HTML の属性は ComponentProps<'button'> で受ける
  • React 19 では ref を props で受けられる。 型は ref?: Ref<HTMLInputElement>
  • useState は推論に任せ、null・空配列・決まった文字列のときだけ型引数を付ける
  • useRef は初期値が必須。 DOM は useRef<HTMLInputElement>(null)、値は useRef<number | undefined>(undefined)
  • イベントは「イベント型+要素の型」。 onSubmit は SubmitEvent、onInput は InputEvent、要素の値は currentTarget から取る
  • よく出るエラーは TS2554(useRef の引数)・TS2503(JSX 名前空間)・TS18047(null の可能性)。 どれも1行で直せる
  • 型は react から import type で取り込む。 JSX も react から import する

参考・出典

  • React 公式ドキュメント「Using TypeScript」 https://react.dev/learn/typescript
  • React 公式ブログ「React 19 Upgrade Guide」 https://react.dev/blog/2024/04/25/react-19-upgrade-guide

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「React TypeScriptの型の書き方|props・イベント型一覧」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?