- React
React TypeScriptの型の書き方|props・イベント型一覧
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期連続の黒字決算。