mcpField
T
typescript-discriminated-unions

typescript-discriminated-unions

github.com/mkosir開発7854.0k 回インストール8 週間前に更新
リポジトリ

typescript-discriminated-unions について

TypeScriptの判別可能なユニオン型の指針を、開発タスクに応じて適用するスキルです。リポジトリ固有の規約を優先し、相互排他的なケースと独立したオプション項目を区別します。

主な機能

  1. 01タスクに関係する指針の適用
  2. 02リポジトリ規約の優先
  3. 03排他的なケースへのユニオン型
  4. 04任意項目とケース依存項目の区別
  5. 05状態や関数引数のモデリング

活用シーン

  1. 01異なるデータを持つ排他的な状態のモデル化
  2. 02異なるケースに応じた関数引数の設計
  3. 03ケース別のReactプロパティの定義

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/mkosir/typescript-style-guide --skill typescript-discriminated-unions

インストール後、Claude Code で/typescript-discriminated-unionsを入力して読み込まれたか確認します。

詳細

判別可能なユニオン

現在のタスクにおいて、TypeScript Style Guide の判別可能なユニオンに関する規約を適用します。

ワークフロー

  1. 利用側リポジトリの規約と設定を確認します。
  2. 明示的なリポジトリの規約がある場合は、この意見に基づくガイダンスより優先します。
  3. タスクに関連するガイダンスのみを適用、確認、または説明します。
  4. 適切なモデルが状況や判断に左右される場合は、重要なトレードオフを示します。

境界

  • 相互に排他的で、異なるデータを必要とするバリエーションには、判別可能なユニオンを使用します。
  • プロパティがそれぞれ独立して存在しない場合や、値だけが変わる場合に、判別可能なユニオンを無理に導入しないでください。
  • 自動的に適用できるチェックは、TypeScript と ESLint に任せます。
  • このスキルが有効だからといって、無関係な TypeScript Style Guide の規約を持ち込まないでください。

判別可能なユニオン {#discriminated-unions}

TypeScript の機能を 1 つだけ選ぶなら、判別可能なユニオンを活用してください。

判別可能なユニオンとは、異なるリテラル値を持つプロパティを共有するオブジェクト型のユニオンです。そのプロパティを確認することで、値の型を該当するバリエーションに絞り込めます。

バリエーションが相互に排他的で、それぞれ異なるデータを必要とする場合は、判別可能なユニオンを使用します。プロパティがそれぞれ独立して存在しない場合はオプショナルのままにし、値だけが変わる場合はリテラルユニオンを使用します。

バリエーションが名前付きの状態やモードを表し、その形状を制御できる場合は、共通のリテラル判別子を優先してください。プロパティの有無そのものが自然な区別となり、判別子を追加すると API がわかりにくくなる場合に限り、オプショナルな never プロパティを使用します。

判別可能なユニオンは、複雑なデータ構造をモデル化し、型安全性を高める強力な概念です。これにより、コードがより明確になり、エラーが起きにくくなります。
C、Haskell、Rust などの言語では、判別可能なユニオンがタグ付きユニオンや直和型など、異なる名前で呼ばれていることがあります(パターンマッチングと組み合わせて使用されます)。

判別可能なユニオンの利点:

  • 必須およびオプショナルなオブジェクトプロパティ、関数の引数、判別可能な型としての Propsで説明したように、判別可能なユニオンは、バリエーションに依存するオプショナルなプロパティを、そのバリエーションで必須のプロパティに置き換え、複雑さを軽減します。

  • 網羅性チェック - 設定された ESLint ルールにより、switch が判別可能なユニオンのすべてのバリエーションを処理していない場合に報告されます。

    {"@typescript-eslint/switch-exhaustiveness-check": "error"}

    type Circle = { kind: 'circle'; radius: number };
    type Square = { kind: 'square'; size: number };
    type Triangle = { kind: 'triangle'; base: number; height: number };
    
    // Create a discriminated union 'Shape', with the 'kind' property to discriminate the type of object.
    type Shape = Circle | Square | Triangle;
    
    const calculateArea = (shape: Shape) => {
      // ESLint reports that the switch is missing the 'triangle' case
      switch (shape.kind) {
        case 'circle':
          return Math.PI * shape.radius ** 2;
        case 'square':
          return shape.size ** 2;
      }
    };
    
  • 相互に排他的な状態を表す複数のブールフラグによって生じるコードの複雑さを回避できます。

  • 型で取り得るケースを明示することで、コードの意図が明確になり、読みやすく理解しやすくなります。

  • TypeScript はユニオン型を絞り込めるため、コンパイル時にコードの正しさを保証できます。

  • 判別可能なユニオンは、関連する型を一元的に定義できるため、リファクタリングや保守が容易になります。ユニオン内の型を追加または変更すると、コンパイラーがコードベース全体の不整合を報告します。

  • IDE は判別可能なユニオンを活用し、より適切な自動補完と型推論を提供できます。

実用例
必須およびオプションのオブジェクトプロパティ {#required--optional-object-properties}

オブジェクトプロパティの大半を必須にし、オプションのプロパティは必要最小限にするよう努めてください。

このアプローチは、型安全で保守しやすいコードの設計につながります。

  • 明確さと予測可能性 — 必須プロパティを使うと、常に必要なデータが明確になります。オブジェクトを使用または利用する開発者にとって曖昧さが減り、何を必ず含めるべきかがはっきりします。
  • 型安全性 — プロパティを必須にすると、TypeScript がその存在を強制し、型チェック時に不足しているプロパティを検出できます。
  • オプショナルチェーンの過剰な使用を回避 — オプションのプロパティが多すぎると、未定義の値に対処するためにオプショナルチェーン(?.)を多用しがちです。これによりコードが煩雑になり、意図が分かりにくくなります。

値が個別に存在しない場合があるときは、オプションのプロパティを使用してください。プロパティの有無がオブジェクトのバリアントによって決まる場合は、判別可能なユニオン型を使用してください。

// ❌ Avoid optional properties when their presence depends on the variant
type User = {
  id?: number;
  email?: string;
  dashboardAccess?: boolean;
  adminPermissions?: ReadonlyArray<string>;
  subscriptionPlan?: 'free' | 'pro' | 'premium';
  rewardsPoints?: number;
  temporaryToken?: string;
};

// ✅ Use a discriminated union so each variant has only its required properties
type AdminUser = {
  role: 'admin';
  id: number;
  email: string;
  dashboardAccess: boolean;
  adminPermissions: ReadonlyArray<string>;
};

type RegularUser = {
  role: 'regular';
  id: number;
  email: string;
  subscriptionPlan: 'free' | 'pro' | 'premium';
  rewardsPoints: number;
};

type GuestUser = {
  role: 'guest';
  temporaryToken: string;
};

// Discriminated union type 'User' ensures clear intent with no optional properties
type User = AdminUser | RegularUser | GuestUser;

const regularUser: User = {
  role: 'regular',
  id: 212,
  email: '[email protected]',
  subscriptionPlan: 'pro',
  rewardsPoints: 1500,
  dashboardAccess: false, // Error: 'dashboardAccess' property does not exist
};
アプリケーションの状態

アプリケーションの状態ごとに異なるデータが必要な場合は、判別可能なユニオンを使って状態とそのデータを一緒にモデル化してください。これにより、データとエラーの両方を保持しながら読み込み中になる、といった無効な組み合わせを防げます。

// ❌ Boolean flags and optional properties allow invalid state combinations
type RequestState = {
  isLoading: boolean;
  data?: Products;
  error?: string;
};

// ✅ Each state contains only the data valid for that state
type RequestState =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: Products }
  | { status: 'error'; error: string };
関数の引数

関数が異なるプロパティを必要とする相互排他的なバリアントを受け取る場合は、判別可能なユニオン型を使用してください。これにより関数の API がシンプルになり、ユースケースごとに必要なプロパティだけが渡されるようになります。

// ❌ Avoid optional properties that allow invalid combinations in the function API
type NotificationParams = {
  channel: 'email' | 'sms';
  email?: string;
  phoneNumber?: string;
  subject?: string;
  message: string;
};

// ✅ Use a discriminated union so each variant requires only its valid properties
type EmailNotificationParams = {
  channel: 'email';
  email: string;
  subject: string;
  message: string;
};

type SmsNotificationParams = {
  channel: 'sms';
  phoneNumber: string;
  message: string;
};

type NotificationParams = EmailNotificationParams | SmsNotificationParams;

export const sendNotification = (params: NotificationParams) => {
  switch (params.channel) {
    case 'email':
      return sendEmail(params.email, params.subject, params.message);
    case 'sms':
      return sendSms(params.phoneNumber, params.message);
  }
};
React の Props
必須およびオプションの Props

Props の大半を必須にし、オプションの Props は必要最小限にするよう努めてください。

特に、初めての、または単一のユースケース向けに新しいコンポーネントを作成する場合は、Props の大半を必須にしてください。コンポーネントがより多くのユースケースに対応するようになったら、それらのユースケース全体で本当に欠ける可能性のある値に限り、オプションの Props を導入してください。
例外として、コンポーネントの API で最初からオプションの Props を実装する必要がある場合もあります(例:複数のユースケースに対応する共有コンポーネント、UI デザインシステムのコンポーネント — button の isDisabled など)。

コンポーネントやフックが複雑になりすぎた場合は、より小さな単位に分割するのが適切です。
極端な例を挙げると、必須の Props を 5 つ持つ React コンポーネントを 10 個実装するほうが、オプションの Props を 50 個受け取る「何でもできる」コンポーネントを 1 つ実装するより望ましいです。

判別可能な型としての Props

コンポーネントのバリアントごとに異なる Props が必要な場合は、判別可能なユニオン型を使用してください。このアプローチにより、コンポーネント API の複雑さが軽減され、バリアントごとに必要な Props だけが渡されるようになります。

// ❌ Avoid optional props that allow invalid combinations in the component API
type AvatarProps = {
  variant: 'image' | 'initials';
  src?: string;
  alt?: string;
  initials?: string;
};

// ✅ Use a discriminated union so each variant requires only its valid props
type ImageAvatarProps = {
  variant: 'image';
  src: string;
  alt: string;
};

type InitialsAvatarProps = {
  variant: 'initials';
  initials: string;
};

type AvatarProps = ImageAvatarProps | InitialsAvatarProps;

export const Avatar = (props: AvatarProps) => {
  switch (props.variant) {
    case 'image':
      return <img src={props.src} alt={props.alt} />;
    case 'initials':
      return <span>{props.initials}</span>;
  }
};

コメント