Menu

ReactとTypeScript:props、state、イベントの型付け

ReactアプリをTypeScriptで型付けする方法です。propsとchildren、ユニオン型やnullを使うuseState、DOM要素のためのuseRef、イベントハンドラ、コンテキスト、ジェネリックなコンポーネント、ネイティブ要素のpropsを扱います。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

ReactとTypeScriptを使うとは、props、state、ref、イベントハンドラに型がある .tsx ファイルでコンポーネントを書くことです。そうすれば、propの書き忘れや間違ったイベントハンドラを、コードを実行する前にコンパイラが見つけてくれます。型はビルド時に消えます。ブラウザで実行されるのは、型なしで書いたのと同じJavaScriptです。このページのライブエディタはそのJavaScriptを実行し、型付きの版はそれぞれの隣の静的なブロックにあります。

次は同じ Badge に型を付けたものです。count は省略可能(?)なので、2つ目のバッジはデフォルトの 0 にフォールバックし、children は要素も普通の文字列も受け付けます。

type BadgeProps = {
    label: string;
    count?: number;
    children: React.ReactNode;
};

function Badge({ label, count = 0, children }: BadgeProps) {
    return (
        <div>
            <strong>{label}</strong> ({count})
            <div>{children}</div>
        </div>
    );
}

<Badge count={3}>Hi</Badge>;
// Error: Property 'label' is missing in type '{ children: string; count: number; }'

TypeScriptのプロジェクトを作る

ViteにはReactとTypeScriptのテンプレートがあります。

npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev

.tsx ファイル、strict が有効な tsconfig.json、そしてインストール済みのReactの型定義(@types/react、@types/react-dom)が手に入ります。Viteは配信とビルドの間に型を取り除きますが、確認はしません。テンプレートの build スクリプトが先に tsc を実行し、エディタは入力中に確認します。Next.jsなどのフレームワークも、CLIでプロジェクトを作るときに同じようにTypeScriptを設定します。

propsに型を付ける

propsをオブジェクトの型として記述し、分割代入した引数に注釈を付けます。ほとんどのコンポーネントは、いくつかのパターンで足ります。

interface UserCardProps {
    name: string;                       // required
    age?: number;                       // optional, may be undefined
    role: 'admin' | 'member';           // a union of allowed strings
    tags: string[];
    onSelect: (id: string) => void;     // a callback prop
    children?: React.ReactNode;         // anything React can render
}

function UserCard({ name, age, role, tags, onSelect, children }: UserCardProps) {
    // ...
}

typeかinterfaceか。どちらもpropsに使えます。interface は extends で拡張でき、2回宣言するとマージされます。type はユニオン型、交差型、マップ型も表現できます。ほとんどのチームはどちらかを選んで、どこでもそれを使います。

children。タグの間に入るもの(要素、文字列、数値、配列、null)には React.ReactNode を使います。そもそも children がどう届くかは、propsのページで説明しています。

React.FC が必須でない理由。const Badge: React.FC<BadgeProps> = (...) => ... は、引数ではなく関数全体に型を付けます。React 18の型からは children を自動で追加しなくなり、ジェネリックなコンポーネントのための型引数も取れず、型付きの引数にない何かを加えるわけでもありません。古いコードでは広く使われていますが、新しいコードでは型付きのpropsを持つ普通の関数がよく選ばれます。

useStateに型を付ける

単純な初期値なら、推論で十分です。useState(0) は number、useState('') は string です。stateが初期値から想像されるより多くのものを持てるときは、型引数を渡します。null で始まる値や、いくつかの決まった文字列のどれかになる値などです。

「Load user」をクリックすると、statusが idle から loading、done へと移ります。型付きのstate:

type User = { id: number; name: string };
type Status = 'idle' | 'loading' | 'done';

const [user, setUser] = useState<User | null>(null);
const [status, setStatus] = useState<Status>('idle');

user.name;          // Error: 'user' is possibly 'null'
user?.name;         // OK
setStatus('ready'); // Error: Argument of type '"ready"' is not assignable to parameter of type 'SetStateAction<Status>'

<User | null> がないと、useState(null) は null 型と推論され、setUser(data) がコンパイルに失敗します。ユニオン型によって空の場合の処理を強制されますが、それこそが動く例の user ? ... : ... の確認です。

DOM要素のためのuseRef

DOM要素を指すrefは、要素の型を取り、null で始まります。最初のレンダリングの後にReactが付けるまで、要素は存在しないからです。

何か入力してから「Log the value」をクリックしてください。DOM要素から直接読んだ、入力した内容がコンソールに出力されます。型付きの版:

const inputRef = useRef<HTMLInputElement>(null);
// inputRef.current is HTMLInputElement | null

inputRef.current?.focus();    // OK
inputRef.current.focus();     // Error: 'inputRef.current' is possibly 'null'

const timerId = useRef<number | null>(null);  // a mutable value, not a DOM node

タグに合った要素のインターフェースを使ってください:HTMLInputElement、HTMLDivElement、HTMLCanvasElement、HTMLButtonElement。React 19の型では、useRef は常に引数を取り、current に代入できる RefObject を返すので、DOMのrefにもタイマーのIDのような値にも同じ書き方が使えます。実行時の側についてはuseRefを参照してください。

イベントハンドラの型

インラインのハンドラには注釈は不要です。onChange={(e) => setName(e.target.value)} では、渡す先のpropから、TypeScriptは e の型を知っています。ハンドラを独立した関数に移すときに注釈を付けます。

送信すると、コンソールにアドレスが記録されます。preventDefault がページの再読み込みを止めます。型付きでは:

function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    setEmail(e.target.value); // e.target is HTMLInputElement, value is string
}

function handleSubmit(e: React.SubmitEvent<HTMLFormElement>) {
    e.preventDefault();
}

function handleClick(e: React.MouseEvent<HTMLButtonElement>) {}
function handleKey(e: React.KeyboardEvent<HTMLInputElement>) {
    if (e.key === 'Enter') { /* ... */ }
}

@types/react 19.2.10から、onSubmit は React.SubmitEvent で型付けされ、古い React.FormEvent は非推奨とされています。古いバージョンの型では、代わりに React.FormEvent<HTMLFormElement> と書いてください。型引数は、ハンドラを付ける要素です。型がわからなければ、エディタで onChange propにカーソルを合わせるか、ハンドラ全体に一度に型を付けてください:const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { ... }。

コンテキストに型を付ける

createContext に値の型を与えます。妥当なデフォルト値があれば渡してください。すべての利用者がnullでない値を受け取れます。

type Theme = 'light' | 'dark';
const ThemeContext = createContext<Theme>('light');

const theme = useContext(ThemeContext); // Theme

よいデフォルト値がないとき(ログイン中のユーザー、ストア)は、null から始め、プロバイダーの外でコンポーネントが使われたら例外を投げるフックで useContext を包みます。そうすればすべての呼び出し側がnullでない型を受け取り、奥深くでクラッシュする代わりにわかりやすいエラーメッセージが得られます。

type Auth = { user: string; logout: () => void };
const AuthContext = createContext<Auth | null>(null);

export function useAuth(): Auth {
    const value = useContext(AuthContext);
    if (!value) throw new Error('useAuth must be used inside <AuthProvider>');
    return value;
}

export function AuthProvider({ children }: { children: React.ReactNode }) {
    const [user, setUser] = useState('Ada');
    return (
        <AuthContext value={{ user, logout: () => setUser('') }}>
            {children}
        </AuthContext>
    );
}

<AuthContext value={...}> はReact 19のプロバイダーの構文で、<AuthContext.Provider value={...}> も同じように型付けされます。

ジェネリックなコンポーネント

どんな項目の型でも動くリスト、テーブル、selectはジェネリックなコンポーネントです。型引数が items propと renderItem コールバックを結びつけます。

型付きの List:

type ListProps<T> = {
    items: T[];
    renderItem: (item: T) => React.ReactNode;
    getKey: (item: T) => string | number;
};

function List<T>({ items, renderItem, getKey }: ListProps<T>) {
    return <ul>{items.map((item) => <li key={getKey(item)}>{renderItem(item)}</li>)}</ul>;
}

<List items={books} getKey={(b) => b.isbn} renderItem={(b) => b.title} />;
// b is inferred as the book type, so b.titel would be an error

TypeScriptは items から T を推論するので、呼び出す側が型引数を書くことはありません。.tsx ファイルでは、<T> だけだとJSXのタグとして読まれてしまうので、アロー関数には <T> の代わりに <T,> が必要です。

ネイティブ要素のpropsを拡張する

button や input を包むラッパーは、ネイティブの要素が受け付けるすべての属性を受け付けるべきです。React.ComponentProps<'button'> がその全体です。その上に自分のpropsを加え、残りをスプレッドします。

type ButtonProps = React.ComponentProps<'button'> & {
    variant?: 'primary' | 'ghost';
};

function Button({ variant = 'primary', style, ...rest }: ButtonProps) {
    return (
        <button
            {...rest}
            style={{ fontWeight: variant === 'primary' ? 600 : 400, ...style }}
        />
    );
}

<Button type="submit" disabled onClick={(e) => console.log(e.currentTarget)}>
    Save
</Button>;

onClick、disabled、type、aria-* は、並べなくてもすべて型付けされています。React 19では ref は普通のpropで、ComponentProps<'button'> に含まれているので、<Button ref={buttonRef}> は forwardRef なしにスプレッドを通じてDOMノードに届きます。ref を除きたいときは ComponentPropsWithoutRef<'button'> を、自分のコンポーネントのpropsを読むには ComponentProps<typeof UserCard> を使ってください。

よくある型エラー

「'ref.current' is possibly 'null'」。最初のレンダリング中は要素が存在しません。ref.current?.focus() を使うか、使う前に if (ref.current) で確認してください。

ユニオン型のstateでの「Type 'string' is not assignable」。const [status, setStatus] = useState('idle') は、望むより広い string と推論され、Status 型のpropに渡すと失敗します。useState<Status>('idle') と書いてください。

間違った要素で型付けされたイベント。select に React.ChangeEvent<HTMLInputElement> を使うと、e.target の型が間違ったものになります。型引数は、ハンドラを付ける要素と一致しなければなりません。

エラーを黙らせるために any を使う。その値が触れるすべてのものについて、確認が無効になります。unknown を使って絞り込むか、型をその出どころで直してください。

よくある質問

TypeScriptでReactアプリを作るには?

npm create vite@latest my-app -- --template react-ts を実行し、続けて npm install と npm run dev を実行します。コンポーネントは .tsx ファイルに置き、Viteはビルド時に型を取り除きます。型を確認するには tsc を実行してください(テンプレートの build スクリプトが実行します)。

Reactのchildrenの型は何ですか?

React.ReactNode です。要素、文字列、数値、配列、null、undefined など、Reactが描画できるものなら何でも受け付けます。要素がちょうど1つ必要なときだけ React.ReactElement を使ってください。

nullを使うuseStateにはどう型を付けますか?

型をユニオン型として明示的に渡します:useState<User | null>(null)。型引数がないと、TypeScriptは許される値を null だけと推論します。

ReactのonChangeイベントの型は何ですか?

inputなら React.ChangeEvent<HTMLInputElement> です(textareaやselectなら HTMLTextAreaElement や HTMLSelectElement)。すると e.target.value は string 型になります。

React.FCを使うべきですか?

どちらでも構いません。propsの引数に直接型を付ける(function Card({ title }: CardProps))方法でも同じことができ、読みやすく、ジェネリクスとも一緒に使えます。React.FC はもう暗黙に children を追加しないので、得られるものはほとんどありません。

propsにはtypeとinterfaceのどちらを使うべきですか?

どちらでも動きます。interface は拡張やマージができ、type はユニオン型やマップ型を表現できます。コードベースで1つの規約を選び、それを守ってください。

Coddyのプログラミング言語のイラスト

Coddyでコードを学ぼう

始める