メインコンテンツまでスキップ

ユニオン分配 (union distribution)

ユニオン分配はジェネリクスで使われる型変数Tに対しユニオン型が指定された場合、その各要素に対してジェネリクスの型変数を適用することを指します。

たとえば、次のような型エイリアスがあります。

ts
type Wrapper<T> = {
value: T;
};
ts
type Wrapper<T> = {
value: T;
};

単なる値をオブジェクト包んだだけですが、このTにユニオン型を代入してみると次のようになります。

ts
type IntOrStr = Wrapper<number | string>;
type IntOrStr = { value: string | number; }
ts
type IntOrStr = Wrapper<number | string>;
type IntOrStr = { value: string | number; }

予想通りの結果が出ますが、実はこれはユニオン分配によってWrapper<number> | Wrapper<string>として評価されたあとの結果を示しています。

ただ、この例だけだとWrapper<number | string> = Wrapper<number> | Wrapper<string>なのでいまいち理解が難しいかもしれませんが、Conditional Typesと併せて使うことによって、より複雑な型を作ることができます。

Distributive Conditional Types

ts
type IsString<T> = T extends string ? true : false;
ts
type IsString<T> = T extends string ? true : false;

この型を使ってstringnumberのユニオン型を持つ変数Tに対してIsStringを適用すると、次のようになります。

ts
type A = IsString<string>;
type A = true
type B = IsString<number>;
type B = false
type C = IsString<string | number>;
type C = boolean
ts
type A = IsString<string>;
type A = true
type B = IsString<number>;
type B = false
type C = IsString<string | number>;
type C = boolean

IsString<T>string型を代入した型エイリアスAtrueとなります。一方string型以外の型を代入した型エイリアスBfalseとなります。

そしてstring | number型を代入した型エイリアスCstring | number型としていっぺんに評価するのではなく、string型とnumber型が個別に評価されます。つまりCIsString<string> | IsString<number>を評価していることと同じになり、結果としてtrue | falseが得られboolean型となります。

もしstring | number型がユニオン分配されずIsStringに適用されたとするとstring | number型はstring型の部分型ではないため、代入することができません。

ユニオン分配を起こさせない方法

ユニオン分配を意図的に起こさせない方法があります。方法は簡単で型変数を[]で囲むだけです。

ts
type NotDistribute<T> = [T] extends [string] ? true : false;
ts
type NotDistribute<T> = [T] extends [string] ? true : false;

このNotDistribute型はstring型に対してはtrueを返しますが、string | number型に対してはfalseを返します。string | number型はstring型の部分型ではないためです。

ts
type A = NotDistribute<string>;
type A = true
type B = NotDistribute<number>;
type B = false
type C = NotDistribute<string | number>;
type C = false
ts
type A = NotDistribute<string>;
type A = true
type B = NotDistribute<number>;
type B = false
type C = NotDistribute<string | number>;
type C = false