mcpField
C
control-union-distribution

control-union-distribution

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

control-union-distribution について

条件型とユニオン型を扱う TypeScript 開発者向けのスキルです。条件型が分配される仕組みと、分配の抑制・強制方法を説明し、boolean や never で予想外の結果になる理由も扱います。

主な機能

  1. 01条件型の分配動作を解説
  2. 02単要素タプルで分配を抑制
  3. 03裸の条件で分配を強制
  4. 04boolean と never の特殊ケース
  5. 05ユニオン入力でのテスト方法

活用シーン

  1. 01条件型の予想外の結果の調査
  2. 02型定義でのユニオン動作の制御
  3. 03再帰的なジェネリック型のデバッグ

インストール

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

$npx skills add https://github.com/marius-townhouse/effective-typescript-skills --skill control-union-distribution

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

詳細

条件型におけるユニオンの分配を制御する

概要

TypeScript の条件型は、デフォルトでユニオンに対して分配されます。通常はこれが望ましい動作ですが、ときには予想外の挙動を引き起こします。分配を制御する方法を理解することは、望ましくないときに分配を防ぎ、必要なときに有効にするうえで、高度な型レベルプログラミングに欠かせません。

意外な挙動として、boolean が true | false として扱われること、never が分配されて never になること、そして再帰型が分配されないことが挙げられます。このスキルでは、こうしたケースへの対処方法を解説します。

このスキルを使う場面

  • 条件型がユニオン入力に対して予想外の動作をする
  • boolean 型が予想外の結果を返す
  • never 型が予想外に評価される
  • ユニオンに対する分配を防ぐ必要がある
  • 再帰的なジェネリック型が正しく分配されない

絶対ルール

条件を 1 要素のタプル [T] で囲むと分配を防げます。裸の条件 N extends... を追加すると分配を強制できます。分配条件型での boolean と never の挙動を理解しましょう。

検出

次のような予想外の挙動に注意してください。

// Surprising: boolean distributes
type Celebrate<V> = V extends true ? 'Huzzah!' : never;
type Surprise = Celebrate<boolean>;  // "Huzzah!" not never

// Surprising: never distributes to never
type AllowIn<T> = T extends { password: string } ? 'Yes' : 'No';
type N = AllowIn<never>;  // never, not 'Yes' | 'No'

// Problem: recursive type doesn't distribute
type NTuple<T, N> = /* ... */;  // NTuple<string, 2 | 3> gives wrong result

分配を防ぐ

条件を 1 要素のタプル [T] で囲みます。

// Problem: distributes over unions
type Comparable<T> =
  T extends Date ? Date | number :
  T extends number ? number :
  T extends string ? string :
  never;

// Date | string becomes (Date | number) | string - wrong!
let dateOrStr: Date | string;
const result: Comparable<typeof dateOrStr>;  // Should be never

// Solution: wrap in one-tuple
type Comparable<T> =
  [T] extends [Date] ? Date | number :
  [T] extends [number] ? number :
  [T] extends [string] ? string :
  never;

// Now Date | string correctly evaluates to never

Boolean の意外な挙動

TypeScript では boolean は true | false として扱われます。

type CelebrateIfTrue<V> = V extends true ? 'Huzzah!' : never;

// Surprising result
type Party = CelebrateIfTrue<true>;      // "Huzzah!"
type NoParty = CelebrateIfTrue<false>;   // never
type Surprise = CelebrateIfTrue<boolean>; // "Huzzah!" (!)

// Why? boolean distributes:
// CelebrateIfTrue<true | false>
// = CelebrateIfTrue<true> | CelebrateIfTrue<false>
// = "Huzzah!" | never
// = "Huzzah!"

// Fix: prevent distribution
type CelebrateIfTrue<V> = [V] extends [true] ? 'Huzzah!' : never;
type SurpriseFixed = CelebrateIfTrue<boolean>;  // never - correct!

Never の意外な挙動

never は空のユニオンとして扱われます。

type AllowIn<T> = T extends { password: string } ? 'Yes' : 'No';

// Surprising: never evaluates to never
type N = AllowIn<never>;  // never (not 'Yes' or 'No')

// Why? never is empty union:
// AllowIn<never> = AllowIn<> = empty union = never

// Fix: wrap in one-tuple
type AllowIn<T> = [T] extends [{ password: string }] ? 'Yes' : 'No';
type NFixed = AllowIn<never>;  // 'No' - correct!

分配を有効にする

分配を強制する必要がある場合もあります。裸の条件を追加します。

// Problem: recursive type doesn't distribute
type NTuple<T, N extends number> = NTupleHelp<T, N, []>;
type NTupleHelp<T, N, Acc extends T[]> =
  Acc['length'] extends N
    ? Acc
    : NTupleHelp<T, N, [T, ...Acc]>;

type PairOrTriple = NTuple<string, 2 | 3>;
// Got: [string, string] (wrong!)
// Want: [string, string] | [string, string, string]

// Solution: add distributive wrapper
type NTuple<T, N extends number> =
  N extends number  // Forces distribution
    ? NTupleHelp<T, N, []>
    : never;

type PairOrTripleFixed = NTuple<string, 2 | 3>;
// Now: [string, string] | [string, string, string] - correct!

完全な例

// Type-safe comparison function
type Comparable<T> =
  [T] extends [Date] ? Date | number :  // Prevent distribution
  [T] extends [number] ? number :
  [T] extends [string] ? string :
  never;

declare function isLessThan<T>(a: T, b: Comparable<T>): boolean;

// Valid comparisons
isLessThan(new Date(), new Date());      // OK
isLessThan(new Date(), Date.now());      // OK (Date/number)
isLessThan(12, 23);                      // OK
isLessThan('A', 'B');                    // OK

// Invalid comparison - correctly rejected
isLessThan(12, 'B');  // Error: string not assignable to number

// Union case - correctly rejected
let dateOrStr: Date | string;
isLessThan(dateOrStr, 'B');  // Error: string not assignable to never

耐圧プロトコル

条件型が予想外の動作をするときは、次の手順に従ってください。

  1. 分配を確認する:分配すべきでない場面で、型がユニオンに対して分配されていますか?
  2. boolean/never でテストする:これらの型は分配の問題を明らかにすることがよくあります
  3. 1 要素のタプルで囲む:[T] extends [X] は分配を防ぎます
  4. 裸の条件を追加する:N extends any は分配を強制します
  5. ユニオンで検証する:ユニオン入力を使って型をテストします

危険信号

症状原因修正方法
boolean が予想外の結果になる分配[T] で囲む
never が never になる空のユニオン[T] で囲む
ユニオンが正しく分割されない分配されていない裸の N extends を追加
インターセクションが必要なのにユニオンになる分配[T] で囲む

よくある言い訳

「複雑なケースでは any を使えばいい」

実際には:分配を理解すれば、型を正確に制御できます。any を使うと、型安全性がすべて失われます。

「このユースケースには複雑すぎる」

実際には:1 要素のタプルを使う方法は簡単です。[T] extends [X] と T extends X の違いを覚えれば、ずっと活用できます。

「型システムはこんなふうに動作すべきではない」

実際には:分配は強力な機能です。これを理解すれば、機能に逆らうのではなく、その力を活用できます。

クイックリファレンス

目的構文例
分配を許可するT extends Xデフォルトの動作
分配を防ぐ[T] extends [X]ユニオン、boolean、never の場合
分配を強制するN extends any ? ... : never再帰型の場合

まとめ

ユニオンに対する分配は通常望ましい動作ですが、常にそうとは限りません。分配を防ぐには [T] extends [X] を使い、強制するには裸の条件を使います。予想外の挙動を避けるために、boolean と never の動作を理解しましょう。

参考資料

  • Dan Vanderkam 著『Effective TypeScript, 2nd Edition』
  • 項目 53:条件型におけるユニオンの分配を制御する方法を知る

コメント