【無料配布】10日間で学べるTypeScript学習資料

【TypeScript】Propsを型安全に定義する(コンポーネント設計の極意)

typescript-props

React(またはNext.js)とTypeScriptを組み合わせて開発する際、コードを書く頻度が高く、かつ設計の腕が問われるのが「Props(プロパティ)」の型定義です。

コンポーネント間でデータを受け渡すPropsに厳格な型を付けることで、実行時エラーを防ぎ、エディタの入力補完(IntelliSense)の恩恵を最大限に受けられます。

本記事では、「TypeScript props」を軸に、基本的な型定義から、HTML属性の拡張、ジェネリクスを用いた高度なコンポーネント設計まで解説します。

目次

コンポーネントのPropsにTypeScriptが必要なのか?

コンポーネントは、外部からPropsという形でデータを受け取り、それをもとにUIを構築します。

JavaScript(型の無い状態)で開発していると、

「親コンポーネントからタイポ(綴り間違い)した名前でPropsを渡してしまった」
「数値が欲しいのに文字列が渡ってきて計算がおかしくなった」

といったバグが、実行して初めて発覚することが多々あります。

TypeScriptを導入し、Propsに型を定義する最大のメリットは以下の2点です。

  • コンパイル時のエラー検知
    必須のPropsを渡し忘れたり、間違った型のデータを渡したりすると、エディタ上で即座に赤い波線で警告してくれます。
  • 開発体験(DX)の劇的な向上
    コンポーネントを呼び出す際、どのようなPropsを渡せばよいのかがエディタのサジェスト(自動補完)で表示されるため、いちいち子コンポーネントのファイルを見に行く必要がなくなります。

TypeScriptでのProps定義の基礎

最も基本的なPropsの定義方法から確認していきましょう。

Props定義の基礎
  • typeinterfaceの使い分け
  • オプショナルなPropsとデフォルト値の設定

typeとinterfaceの使い分け

Propsの型を定義する際、type(型エイリアス)を使うべきか、interfaceを使うべきか迷う方は多いです。

// typeを使用する場合 (最近の主流)
type ButtonProps = {
  label: string;
  color: string;
};

// interfaceを使用する場合
interface ButtonProps {
  label: string;
  color: string;
}

const Button = ({ label, color }: ButtonProps) => {
  return <button style={{ backgroundColor: color }}>{label}</button>;
};

2026年現在のモダンなReact開発においては、typeを使用するのが主流となっています。

type&(交差型)や|(ユニオン型)を使った柔軟な合成が得意であり、コンポーネントのProps定義において直感的に扱いやすいためです。

ただし、チームの規約でinterfaceに統一されている場合はそれに従いましょう。

オプショナルなPropsとデフォルト値の設定

必ずしも渡されるとは限らないPropsには、プロパティ名の後ろに?をつけてオプショナル(省略可能)にします。

オプショナルなPropsを扱う際は、分割代入時にデフォルト値を設定するのがベストプラクティスです。

type GreetingProps = {
  name: string;
  message?: string; // 渡されなくてもエラーにならない
};

// 分割代入でデフォルト値を設定する
const Greeting = ({ name, message = "こんにちは!" }: GreetingProps) => {
  return (
    <div>
      <p>{name}さん、{message}</p>
    </div>
  );
};

// 呼び出し側
<Greeting name="田中" /> // "田中さん、こんにちは!" と表示される

このように記述することで、コンポーネント内部でmessageundefinedになることを防ぎ、安全に処理を進められます。

React特有の型を使いこなす

Reactコンポーネントには、通常のJavaScriptオブジェクトにはない特有のPropsが存在します。

React特有の型
  • childrenプロパティの最適な型定義(ReactNode
  • イベントハンドラの型定義(MouseEvent, ChangeEvent
  • React.FCは使うべきか?

childrenプロパティの最適な型定義(ReactNode)

コンポーネントタグで囲んだ要素を受け取るchildrenは、頻繁に使われるPropsの一つです。

childrenの型定義には、すべてのReact要素(文字列、数値、JSX要素、それらの配列など)を許容するReact.ReactNodeを使用するのが最適解です。

import React, { ReactNode } from 'react';

type CardProps = {
  title: string;
  children: ReactNode; // childrenにはReactNodeを指定
};

const Card = ({ title, children }: CardProps) => {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-content">{children}</div>
    </div>
  );
};

JSX.ElementReactElementは許容範囲が狭く(文字列などを弾いてしまう)、childrenの型としては不便な場面が多いため注意が必要です。

イベントハンドラの型定義(MouseEvent, ChangeEvent)

ボタンのクリックや入力フォームの変更など、関数をPropsとして渡す際の型定義も重要です。

import React, { MouseEvent, ChangeEvent } from 'react';

type FormProps = {
  // 入力変更時のハンドラ (引数にChangeEventをとる)
  onChange: (e: ChangeEvent<HTMLInputElement>) => void;
  // クリック時のハンドラ (引数にMouseEventをとる)
  onClick: (e: MouseEvent<HTMLButtonElement>) => void;
  // 引数なしのシンプルな関数
  onSubmit: () => void;
};

イベントオブジェクト(e)の型は、発生元となるHTML要素(HTMLInputElementなど)をジェネリクスとして指定することで、e.target.valueなどに型安全にアクセスできます。

React.FCは使うべきか?

かつては関数コンポーネントを定義する際、React.FC(Functional Component)という型を使うのが流行しました。

// 過去によく見られた書き方 (現在は非推奨寄り)
const LegacyComponent: React.FC<MyProps> = ({ title }) => { ... };

しかし2026年現在、React.FCを使用することは非推奨寄りになっています。

理由は以下の通りです。

  • 過去バージョンではchildrenを含んでしまい、バグの温床になっていた。(React 18以降で修正済み)。
  • ジェネリクスを使ったコンポーネント(後述)とうまく組み合わない。
  • 単純に関数の引数に型をつける方がシンプルで直感的。
const ModernComponent = ({ title }: MyProps) => { ... };

引数に直接Propsの型を注釈するスタイルを基本としましょう。

HTML標準属性を拡張(継承)するProps設計

カスタムボタンやインプットを作る際、typedisabledonClickなど、標準のHTML要素が持つ属性はそのまま受け取りたいというケースがあります。

HTML標準属性を拡張するProps設計
  • ComponentPropsを用いた属性の透過
  • Omitを活用した特定属性の排除と上書き

ComponentPropsを用いた属性の透過

一つ一つの属性を自前で定義するのは非現実的です。

そこでReact.ComponentPropsWithoutRef(または ComponentProps)を使用します。

import { ComponentPropsWithoutRef } from 'react';

// buttonタグが持つすべての属性(onClick, disabled, type等)を受け継ぐ
type CustomButtonProps = ComponentPropsWithoutRef<"button"> & {
  variant?: 'primary' | 'secondary'; // 独自のPropsを追加
};

const CustomButton = ({ variant = 'primary', className, ...props }: CustomButtonProps) => {
  const btnClass = variant === 'primary' ? 'bg-blue-500' : 'bg-gray-500';
  
  return (
    // ...props で残りの属性(onClickなど)をすべてbuttonに渡す
    <button className={`${btnClass} ${className}`} {...props} />
  );
};

<CustomButton onClick={...} disabled />のように、標準の<button>と同じ使い勝手でありながら、スタイルや機能を持ったコンポーネントを作成できます。

Omitを活用した特定属性の排除と上書き

標準属性を受け継ぎつつ、特定の属性だけは型の条件を厳しくしたいまたは独自のものに差し替えたい場合は、TypeScriptのユーティリティ型であるOmitを活用します。

// inputの標準属性から 'type' と 'onChange' を除外する
type BaseInputProps = Omit<ComponentPropsWithoutRef<"input">, 'type' | 'onChange'>;

type CustomInputProps = BaseInputProps & {
  // typeを2種類に限定して上書き
  type: 'text' | 'password'; 
  // onChangeの引数をEventではなく文字列(value)に変更して上書き
  onChange: (value: string) => void; 
};

Omit&(交差型)を組み合わせることで、HTML要素の振る舞いをコントロールした高度なUIコンポーネントを設計できます。

複雑な要件を満たすPropsパターン

実務では、単なるデータの受け渡し以上の複雑な制約を持ったコンポーネントを設計することが求められます。

要件を満たすPropsパターン
  • ジェネリクスを用いた汎用コンポーネント
  • 判別可能なユニオンによる条件分岐

ジェネリクスを用いた汎用コンポーネント

「どんな型の配列でも受け取れて、リスト表示するコンポーネント」を作りたい場合、ジェネリクスを使用します。

type ListProps<T> = {
  items: T[];
  renderItem: (item: T) => React.ReactNode; // itemsの中身の型に依存する
};

// コンポーネント定義に <T,> をつける (TSXでの構文エラー回避のためカンマが必要)
const GenericList = <T,>({ items, renderItem }: ListProps<T>) => {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{renderItem(item)}</li>
      ))}
    </ul>
  );
};

呼び出し側では、渡した配列の型が自動的に推論されます。

// usersの型(User)が自動的に T に当てはまるため、renderItem内の user は型補完が効く
<GenericList
  items={users}
  renderItem={(user) => <span>{user.name}</span>} 
/>

判別可能なユニオンによる条件分岐

「アラートコンポーネントで、typeが ‘success’の時はmessageが必要だが、’error’の時はerrorIdも必要になる」といった他のPropsの値によってPropsが変わるパターンです。

type SuccessAlertProps = {
  type: 'success';
  message: string;
};

type ErrorAlertProps = {
  type: 'error';
  message: string;
  errorId: number; // errorの時だけ必須
};

// 2つの型をユニオン(|)で結ぶ
type AlertProps = SuccessAlertProps | ErrorAlertProps;

const Alert = (props: AlertProps) => {
  if (props.type === 'error') {
    // ここではTypeScriptが props を ErrorAlertProps だと推論するため、errorIdにアクセス可能
    return <div>エラー {props.errorId}: {props.message}</div>;
  }
  return <div>成功: {props.message}</div>;
};

この手法(Discriminated Unions)を用いることで、コンポーネントを使う側が「あり得ないPropsの組み合わせ」を渡すことをコンパイルレベルで完全にブロックできます。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次