0Pricing
React Academy · レッスン

Component VariantsのDiscriminated Unions

discriminated unionsでvariant propsをモデル化し、TypeScriptに有効なpropの組み合わせを強制させます。

「Component VariantsのDiscriminated Unions」はCoddyKit上の無料React Academyレッスンです。 これはレッスン1/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはReact Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 React Academyコースには全4レッスンが含まれています。

バリアントpropsの問題

異なるモードに対応するオプショナルなpropsを持つコンポーネント(リンクにもボタンにもなるボタンなど)では、無効なpropsの組み合わせが生じることがあります。TypeScriptは、判別共用体を使わなければそれを検出できません。

判別共用体とは

判別共用体は、共通のリテラル型フィールド(判別子)を持つ型の共用体です。TypeScriptは、そのフィールドの値に基づいて型を絞り込みます。

type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'rectangle'; width: number; height: number };

function area(shape: Shape): number {
  if (shape.kind === 'circle') return Math.PI * shape.radius ** 2;
  return shape.width * shape.height; // TS knows width/height exist here
}

ButtonとLinkのバリアント

ポリモーフィックコンポーネントでは、asまたはvariantフィールドの判別共用体を使って、各ケースに正しいpropsが適用されるようにします。

type ButtonProps =
  | { as: 'button'; onClick: () => void; disabled?: boolean; children: React.ReactNode }
  | { as: 'a'; href: string; target?: string; children: React.ReactNode };

function ActionButton(props: ButtonProps) {
  if (props.as === 'button') {
    return <button onClick={props.onClick} disabled={props.disabled}>{props.children}</button>;
  }
  return <a href={props.href} target={props.target}>{props.children}</a>;
}

Alertコンポーネントのバリアント

severityの種類ごとに異なる必須データを持つAlertをモデル化します。

type AlertProps =
  | { type: 'success'; message: string }
  | { type: 'error'; message: string; onRetry: () => void }
  | { type: 'warning'; message: string; details?: string };

function Alert(props: AlertProps) {
  if (props.type === 'error') {
    return (
      <div className="alert error">
        <p>{props.message}</p>
        <button onClick={props.onRetry}>Retry</button>
      </div>
    );
  }
  return <div className={`alert ${props.type}`}>{props.message}</div>;
}

イベントハンドラーでの型の絞り込み

判別共用体はイベント駆動のデータにも利用できます。ステートマシンやアクションベースの状態管理に最適です。

type LoadingState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

function DataView<T>({ state }: { state: LoadingState<T> }) {
  if (state.status === 'loading') return <Spinner />;
  if (state.status === 'error') return <p>{state.error.message}</p>;
  if (state.status === 'success') return <pre>{JSON.stringify(state.data)}</pre>;
  return null;
}

neverによる網羅性チェック

default分岐にneverのチェックを追加すると、新しい共用体メンバーが追加されたのに処理されていない場合、TypeScriptエラーが発生します。

function assertNever(x: never): never {
  throw new Error('Unhandled case: ' + x);
}

function renderIcon(type: AlertProps['type']) {
  switch (type) {
    case 'success': return <CheckIcon />;
    case 'error': return <XIcon />;
    case 'warning': return <WarnIcon />;
    default: return assertNever(type); // TS error if a case is missing
  }
}

APIレスポンスの判別共用体

APIレスポンスの形状を判別共用体としてモデル化すると、呼び出し側で型アサーションを使わずに成功時とエラー時の経路を処理できます。

type ApiResult<T> =
  | { ok: true; data: T }
  | { ok: false; error: string; code: number };

async function fetchUser(id: string): Promise<ApiResult<User>> {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) return { ok: false, error: 'Not found', code: res.status };
  return { ok: true, data: await res.json() };
}

絞り込みのための型ガード

判別子が単純な等価性チェックではない複雑なケースでは、カスタム型ガードを使って共用体を絞り込みます。

function isSuccess<T>(result: ApiResult<T>): result is { ok: true; data: T } {
  return result.ok === true;
}

const result = await fetchUser('1');
if (isSuccess(result)) {
  console.log(result.data.name); // TS knows data exists
}

オプショナルpropsの氾濫を避ける

判別共用体を使わないと、特定の組み合わせでしか有効でないオプショナルなpropsがコンポーネントに増え、分かりにくく型付けも不十分になります。判別共用体を使えば、あり得ない状態を排除できます。

// Bad: optional prop soup — invalid combos allowed:
interface BadProps {
  href?: string;
  onClick?: () => void;
  disabled?: boolean;
}

// Good: only valid combos via discriminated union:
type GoodProps =
  | { as: 'a'; href: string }
  | { as: 'button'; onClick: () => void; disabled?: boolean };

共用体を合成する

&(交差型)を使うと、共用体のすべてのメンバーに共通propsを追加できます。

type BaseProps = { className?: string; children: React.ReactNode };

type ButtonVariant =
  | (BaseProps & { variant: 'primary'; onClick: () => void })
  | (BaseProps & { variant: 'link'; href: string });

実行時の判別

Reactは実行時に判別子を使って適切なUIをレンダリングします。TypeScriptはコンパイル時に判別子を使って、正しいpropsの使用を強制します。両方の層で安全性が保たれます。

確認問題

判別共用体における判別子とは何ですか?

まとめ

判別共用体は、リテラル型の判別子フィールドを共有することで、互いに排他的なpropsの集合を持つコンポーネントをモデル化します。網羅性チェックにはdefault分岐でneverを使用します。オプショナルなpropsでは許されてしまう無効なpropsの組み合わせを排除できるため、コンポーネントが自己文書化され、型安全になります。

よくある質問

「Component VariantsのDiscriminated Unions」レッスンは無料ですか?

はい。「Component VariantsのDiscriminated Unions」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、React Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 React Academyコースには全4レッスンが含まれています。

「Component VariantsのDiscriminated Unions」で何を学びますか?

discriminated unionsでvariant propsをモデル化し、TypeScriptに有効なpropの組み合わせを強制させます。 ブラウザで直接実行するハンズオンコードでReact Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

React Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのReact Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン1/4です。

「Component VariantsのDiscriminated Unions」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このReact Academyレッスンでコードを書いて実行できますか?

はい。すべてのReact Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. Component VariantsのDiscriminated Unions
  2. ReactにおけるConditional TypesとMapped Types
  3. 'as' Propを使ったPolymorphic Components
  4. 型安全なFormsとAPI Response Contracts
← React Academyに戻る