mcpField
E
exclusive-or-properties

exclusive-or-properties

github.com/marius-townhouse開発646 回インストール39 週間前に更新
リポジトリ

exclusive-or-properties について

exclusive-or-properties は、設定やコンポーネントのプロパティ、API パラメーターの排他条件を扱う TypeScript 開発者向けスキルです。オプショナルな never プロパティとユニオン型により、選択肢がちょうど 1 つであることをコンパイル時に制約します。

主な機能

  1. 01オプショナルな never プロパティで排他性を強制
  2. 02ユニオン型で有効なバリアントを定義
  3. 03コンポーネントのプロパティと API パラメーターを扱う
  4. 04再利用可能な XOR ヘルパー型を提示
  5. 05組み合わせをコンパイル時に検査

活用シーン

  1. 01設定で選択元を 1 つだけ必須にする
  2. 02コンポーネントの操作方式を 1 つに制限
  3. 03排他的な API パラメーター形式をモデル化

インストール

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

$npx skills add https://github.com/marius-townhouse/effective-typescript-skills --skill exclusive-or-properties

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

詳細

排他的論理和をモデル化するためにオプショナルな never プロパティを使用する

概要

複数のプロパティのうち、必ずちょうど1つだけが存在し、2つ以上は存在しない型が必要になることがあります。この「排他的論理和」(XOR)パターンは、コンポーネントのプロパティ、API パラメーター、設定オブジェクトでよく使われます。never 型を使ったオプショナルプロパティにより、この制約をコンパイル時に適用できます。

この手法は、オブジェクトのユニオンよりも型安全性が高く、実行時チェックよりも意図が明確になります。

この手法を使う場面

  • 複数のプロパティのうち、必ずちょうど1つだけが存在する必要がある場合
  • 相互排他的な設定オプションをモデル化する場合
  • 代替設定を持つコンポーネントのプロパティ
  • 排他的なバリエーションを持つ API パラメーター
  • プロパティの無効な組み合わせを防ぐ場合

鉄則

「必ずちょうど1つだけ」の制約を適用するには、オプショナルな never プロパティを使用します。各バリエーションでは、対応するプロパティを必須にし、それ以外を never にします。

検出

次のような無効な組み合わせに注意してください。

// RED FLAGS - Invalid combinations allowed
interface Config {
  url?: string;
  filePath?: string;
  content?: string;
}
// Can pass none, one, or all - too permissive!

// Runtime validation needed:
if ((config.url ? 1 : 0) + (config.filePath ? 1 : 0) + (config.content ? 1 : 0) !== 1) {
  throw new Error('Exactly one source required');
}

問題点

interface LoadConfig {
  url?: string;      // Load from URL
  filePath?: string; // Load from file
  content?: string;  // Load from string
}

// All these are allowed, but shouldn't be:
const bad1: LoadConfig = {};  // No source specified
const bad2: LoadConfig = { url: '...', filePath: '...' };  // Two sources
const bad3: LoadConfig = { url: '...', filePath: '...', content: '...' };  // All three

// Runtime check required
function load(config: LoadConfig) {
  const sources = [config.url, config.filePath, config.content].filter(Boolean);
  if (sources.length !== 1) {
    throw new Error('Exactly one source required');
  }
  // ...
}

解決策:オプショナルな never プロパティ

type LoadConfig =
  | { url: string; filePath?: never; content?: never }
  | { url?: never; filePath: string; content?: never }
  | { url?: never; filePath?: never; content: string };

// Valid - exactly one source:
const good1: LoadConfig = { url: 'https://example.com' };
const good2: LoadConfig = { filePath: '/path/to/file' };
const good3: LoadConfig = { content: 'raw content' };

// Invalid - caught at compile time:
const bad1: LoadConfig = {};  // Error: missing required property
const bad2: LoadConfig = { url: '...', filePath: '...' };  // Error: filePath must be undefined
const bad3: LoadConfig = { url: '...', content: '...' };   // Error: content must be undefined

コンポーネントのプロパティの例

type ButtonProps = {
  label: string;
  onClick: () => void;
} & (
  | { href: string; to?: never; onPress?: never }      // External link
  | { href?: never; to: string; onPress?: never }     // Router link
  | { href?: never; to?: never; onPress: () => void } // Custom action
);

// Usage
<Button label="External" href="https://example.com" />
<Button label="Internal" to="/dashboard" />
<Button label="Action" onPress={handlePress} />

// Error: can't combine variants
<Button label="Bad" href="..." to="..." />  // Error!

API パラメーターの例

type SearchParams = {
  limit?: number;
  offset?: number;
} & (
  | { query: string; filters?: never }
  | { query?: never; filters: Filter[] }
);

// Valid
search({ query: 'typescript' });
search({ filters: [{ field: 'status', value: 'active' }] });
search({ query: 'typescript', limit: 10 });

// Invalid
search({});  // Error: need query or filters
search({ query: '...', filters: [...] });  // Error: can't have both

汎用 XOR ヘルパー

type XOR<A, B> =
  | (A & { [K in keyof B]?: never })
  | (B & { [K in keyof A]?: never });

// Usage
type Config = XOR<
  { url: string },
  XOR<
    { filePath: string },
    { content: string }
  >
>;

// Or for exactly one of many:
type ExactlyOne<T> = {
  [K in keyof T]: { [P in K]: T[K] } & {
    [P in Exclude<keyof T, K>]?: never;
  };
}[keyof T];

type LoadConfig2 = ExactlyOne<{
  url: string;
  filePath: string;
  content: string;
}>;

プレッシャー耐性プロトコル

排他的なプロパティを適用する際は、次の手順に従います。

  1. 排他性を特定する:どのプロパティが相互排他的ですか?
  2. バリエーションを作成する:各バリエーションでは、1つを必須にし、それ以外を never にします
  3. バリエーションをユニオンにする:| 演算子で組み合わせます
  4. 組み合わせをテストする:無効な組み合わせでエラーになることを確認します
  5. パターンを文書化する:特定の組み合わせが無効な理由を説明します

危険信号

アンチパターン問題点解決策
すべてのプロパティがオプショナル何も指定しないことも、複数指定することも可能になりますオプショナルな never パターン
実行時の検証のみコンパイル時ではなく、実行時にエラーになります型レベルで適用する
「1つだけ使用すること」というコメント制約が適用されません型エラーになるようにする

よくある言い訳

「実行時に検証します」

実際のところ:実行時の検証では、バグの発見が遅くなります。型レベルでの適用なら、すぐに検出できます。

「冗長すぎます」

実際のところ:XOR や ExactlyOne などのヘルパー型を使えば、繰り返しを減らせます。

「ユーザーは複数のオプションを使いたいかもしれません」

実際のところ:それが有効なら XOR は使いません。有効でないなら、型レベルで適用します。

クイックリファレンス

パターン構文意味
XOR(プロパティが2つ){ a: T, b?: never } | { a?: never, b: T }ちょうど1つ
XOR(プロパティが3つ以上)バリエーションのユニオンちょうど1つ
ヘルパー型XOR<A, B>再利用可能なパターン

まとめ

コンパイル時に「必ずちょうど1つだけ」の制約を適用するには、オプショナルな never プロパティを使用します。これにより、実行時エラーの一種を根本から排除し、無効な状態を表現できないようにします。

参考資料

  • Dan Vanderkam 著『Effective TypeScript』第2版
  • 項目63:排他的論理和をモデル化するためにオプショナルな Never プロパティを使用する

コメント