Back

TSX と型付きフロントエンドコンポーネントの台頭

TSX と型付きフロントエンドコンポーネントの台頭

React コンポーネントを構築しているとします。数値を渡すべきところに文字列を渡してしまった場合、TypeScript がブラウザよりも先にそれを検出します。この単純なやり取り—型がランタイムエラーを防ぐ—が、TSX が現代のフロントエンド開発におけるデフォルトフォーマットになった理由を説明しています。

本記事では、型付きフロントエンドコンポーネントが実際に何を意味するのかを取り上げます:props の型付け、イベント処理、children パターン、そして TypeScript の型推論がどのように定型コードを排除するか。また、React 19 のサーバーコンポーネントとクライアントコンポーネントの分離が、型付け戦略にどのような影響を与えるかについても説明します。

重要なポイント

  • TSX 型付きコンポーネントはコンパイル時に型の不一致を検出し、コードがブラウザに到達する前にランタイムエラーを削減します。
  • TypeScript の型推論はほとんどのイベント型付けを自動的に処理します—明示的な型注釈が必要なのは、ハンドラを別の関数に抽出する場合のみです。
  • 判別共用体により、相互排他的な動作を持つ型安全なコンポーネントが可能になり、TypeScript は判別プロパティに基づいて型を絞り込みます。
  • React 19 のサーバー/クライアント境界はシリアライゼーション制約を導入します:サーバーコンポーネントからクライアントコンポーネントに渡される props はシリアライズ可能でなければなりません(関数、クラス、シンボル、その他のシリアライズ不可能な値は使用できません)。

TSX 型付きコンポーネントが実際に意味すること

型付きコンポーネントは、単に型付けされた props を持つコンポーネントではありません。TypeScript が契約全体を理解するコンポーネントです:何が入力され、何が出力され、その間に何が起こるか。

モダンな JSX トランスフォーム(react-jsx)では、すべてのファイルで React をインポートする必要はありません。TSX を記述すれば、ツールチェーンが残りを処理します。Vite は、これを最初から正しくセットアップします:

interface ButtonProps {
  label: string
  disabled?: boolean
}

function Button({ label, disabled = false }: ButtonProps) {
  return <button disabled={disabled}>{label}</button>
}

ここに React.FC がないことに注目してください。このパターンはオプションであり、多くの場合不要です。関数パラメータに直接 props を型付けする方がクリーンで、必要かどうかに関わらず React.FC が追加する暗黙的な children prop を避けることができます。

Props の型付けと React TSX の型安全性

Props の型付けは、ほとんどの開発者が最初に取り組むところですが、React TSX の型安全性はさらに深いものです。相互排他的な動作を持つコンポーネントには判別共用体を検討してください:

type LinkButtonProps = 
  | { variant: 'button'; onClick: () => void }
  | { variant: 'link'; href: string }

function LinkButton(props: LinkButtonProps) {
  if (props.variant === 'link') {
    return <a href={props.href}>Navigate</a>
  }
  return <button onClick={props.onClick}>Click</button>
}

TypeScript は variant に基づいて型を絞り込みます。variant'button' の場合、誤って href にアクセスすることはできません。このパターンは複雑なコンポーネント API にも適しています。

定型コードなしのイベント型付け

イベントハンドラは、TypeScript を初めて使う開発者がつまずくことが多い部分です。良いニュースは:型推論がほとんどのケースを自動的に処理します。

import { useState } from 'react'

function SearchInput() {
  const [query, setQuery] = useState('')

  // TypeScript はコンテキストからイベント型を推論します
  return (
    <input 
      value={query} 
      onChange={(e) => setQuery(e.currentTarget.value)} 
    />
  )
}

ハンドラを別の関数に抽出する場合は、明示的な型が必要になります:

import type { ChangeEvent } from 'react'

function handleChange(event: ChangeEvent<HTMLInputElement>) {
  // event.currentTarget.value は正しく string として型付けされます
}

Children の型付け: ReactNode vs ReactElement

children には、React.ReactNode が一般的なケースをカバーします—レンダリング可能なあらゆるもの:

import type { ReactNode } from 'react'

interface CardProps {
  children: ReactNode
}

厳密に JSX 要素が必要で、文字列や数値を除外したい場合は React.ReactElement を使用します。children を受け入れるコンポーネントには、PropsWithChildren が手動で children prop を追加するよりもクリーンな代替手段です:

import type { PropsWithChildren } from 'react'

interface CardProps {
  title: string
}

function Card({ title, children }: PropsWithChildren<CardProps>) {
  return (
    <div>
      <h2>{title}</h2>
      {children}
    </div>
  )
}

React 19 におけるサーバーコンポーネントとクライアントコンポーネントの型付け

React 19 TypeScript プロジェクト、特に Next.js App Router を使用するものでは、コンポーネントがサーバー境界とクライアント境界に分割されます。これは型付けに影響します。

サーバーコンポーネントは非同期にでき、直接データを取得できます:

// サーバーコンポーネント - ディレクティブは不要(App Router ではデフォルト)
async function UserProfile({ userId }: { userId: string }) {
  const user = await fetchUser(userId)
  return <div>{user.name}</div>
}

クライアントコンポーネントには 'use client' ディレクティブが必要で、フックを使用できます:

'use client'

import { useState } from 'react'

interface CounterProps {
  initialCount: number
}

function Counter({ initialCount }: CounterProps) {
  const [count, setCount] = useState(initialCount)
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

型付け境界は重要です:サーバーコンポーネントからクライアントコンポーネントに渡される props はシリアライズ可能でなければなりません。関数、クラス、シンボル、シリアライズ不可能な値(クラスインスタンスや Date など)は、その境界を越えることができません。

フォームアクションとモダンな型付きワークフロー

React 19 は、サーバーアクション(フォームで一般的に使用される)を、送信処理のための型付きパターンとして導入しました。通常、サーバーファイルで定義され、クライアントコンポーネントから呼び出されます:

async function submitForm(formData: FormData) {
  'use server'
  const email = formData.get('email')
  if (typeof email !== 'string') {
    throw new Error('Email is required')
  }
  // 送信を処理
}

これは、クライアントからサーバーまでフォーム処理を型認識に保つことで、型付きフロントエンドコンポーネントと統合されます。

まとめ

TSX 型付きコンポーネントは、コンパイル時に不一致を検出することでバグを削減します。TypeScript の型推論は定型コードを最小限に抑えます—フックやインラインハンドラに明示的な型注釈が必要になることはほとんどありません。React 19 のサーバー/クライアント分離は、新たな考慮事項を追加します:シリアライゼーション境界です。

特定の理由がない限り React.FC はスキップしてください。props を直接型付けしてください。型推論に任せてください。エディタは正確な自動補完と即座のエラーフィードバックで応えてくれます。

よくある質問

関数パラメータに直接 props を型付けしてください。React.FC は必要かどうかに関わらず暗黙的な children prop を追加し、props を直接型付けする方がクリーンです。React チームは React.FC をデフォルトパターンとして推奨していません。それを期待するレガシーコードベースで作業する場合など、その動作が特に必要な場合にのみ使用してください。

インラインハンドラの場合、TypeScript はコンテキストからイベント型を自動的に推論します。ハンドラを別の関数に抽出する場合は、入力変更には React.ChangeEvent、クリックには React.MouseEvent などの明示的な型を使用します。ジェネリックパラメータは、HTMLInputElement や HTMLButtonElement などの要素型を指定します。

ReactNode は、文字列、数値、null、undefined、JSX 要素を含むレンダリング可能なあらゆるものを受け入れます。ReactElement はプリミティブを除外し、JSX 要素のみを受け入れます。ほとんどの場合は ReactNode を使用してください。コンポーネントが特に JSX children を必要とし、プレーンテキストや数値を拒否する必要がある場合は ReactElement を使用してください。

いいえ。サーバーコンポーネントからクライアントコンポーネントに渡される props はシリアライズ可能でなければなりません。関数、クラス、シンボル、その他のシリアライズ不可能な値は、サーバー-クライアント境界を越えることができません。イベントハンドラとコールバックはクライアントコンポーネント内で定義するか、サーバー側処理が必要なフォーム送信にはサーバーアクションを使用してください。

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay