Union Distribution とは
Union Distribution とは、Conditional Types と呼ばれる型の型引数(ジェネリック型)にユニオン型が渡された際に、そのユニオン型に対して分配(distribution)が行われるという性質のことです。
本記事では、Conditional Types の構文についてと、ユニオン型を渡した際にどのように型が得られるのかについて見ていきたいと思います。
Conditional Types について
Conditional Types とは、条件分岐によって表せる型のことで、T extends U ? A : B
という構文で表されます(T
は型引数になります)。
T extends U ? A : B
は、T
型がU
型の部分型であればA
型を、そうでなければB
型となる型を表します。
(T extends U
の部分をboolean のように捉えると、三項演算子と同じ構造になっていますね。)
type IsString<T> = T extends string ? true : false
上の型は、T
がstring
型の部分型であればtrue
型となり、そうでなければfalse
型となります。
ユニオン型を渡すと分配が起こるDistributive Conditional Types
Conditional Types の型引数にユニオン型を渡す形については特別な名前がついており、Distributive Conditional Typesと呼ばれます。
以下で、型引数T
がstring
型の場合にT
の配列型を返す型ToArray<T>
を考えてみます。
type ToArray<T> = T extends string ? T[] : never
//type UnionArray = "octopus"[] | "squid"[]
type UnionArray = ToArray<"octopus"|"squid">
ToArray<T>
型にstring
型の部分型であるようなユニオン型"octopus"|"squid"
を渡した際に得られる型を見てみます。
UnionArray
の型としては、構文を文字通りみていくと"(octopus"|"squid")[]
という型になりそうですが、Union Distribution が働くため、実際にはユニオン型の構成要素が分配される形となり、"octopus"[] | "squid"[]
型となります。
ちなみに"(octopus"|"squid")[]
のようなユニオン型の配列型を得たい場合には、以下のコード例のようにextends
の前後を[]
で囲むことでUnion Distribution が働かないようにできます。
type X<T> = [T] extends [string] ? T[] : never
type Y = X<"octopus" | "squid" > //type Y = ("octopus" | "squid")[]
もしくはT
の部分を[T]
とすることでも(煩雑になりますが)取得可能ではあります。
type X<T> = T extends string ? [T] : never
//type Y = ("octopus" | "squid")[]
type Y = X<"octopus" | "squid" >[number][]
ユーティリティ型で使われるDistributive Conditional Types
Distributive Conditional Types は少々難解なため、使用頻度はそこまで高くはないかもしれませんが、Extract
型やExclude
型といった組み込み型がどのように実装されているのを理解するのに役立ちます。
以下はExclude
型とExtract
型の実装部分で、Distributive Conditional Types が利用されています。
/**
* Exclude from T those types that are assignable to U
*/
type Exclude<T, U> = T extends U ? never : T;
/**
* Extract from T those types that are assignable to U
*/
type Extract<T, U> = T extends U ? T : never;
Extract の動きを追う
Extract
型がどのように型を決定しているのかをみていきたいと思います。
Extract<T, U>
型は、T
型からU
型の部分型を抽出するユーティリティ型です。
以下のように'octopus' | 'squid'
型と'octopus' | 'tuna'
型をExtract
に渡してみると、'octopus'
型が抽出されます。
type Octopus = Extract<'octopus' | 'squid', 'octopus' | 'tuna'>
//type Octopus = "octopus"
どのようにUnion Distribution が起きているかを、Extract
の形を少し変えてみていきます。
Extract<'octopus' | 'squid', 'octopus' | 'tuna'>
を、Exclude<'octopus' | 'squid', T>
として考えてみます。
以下のコードではOctopusSquid<T>
にあたります。
type OctopusSquid<T> = T extends 'octopus' | 'squid' ? T : never
type X = OctopusSquid<'octopus' | 'tuna'>
//type X = "octopus"
OctopusSquid<T>
の型引数T
に'octopus' | 'tuna'
型を渡すと、Union Distribution によって、各要素ごとに条件判定がなされる形となります。
まず、'octopus' | 'tuna'
の'octopus'
部分について、こちらは'octopus' | 'squid'
の部分型となっているため、T extends 'octopus' | 'squid'
の条件を満たしており、T
型、つまり'octopus'
型と判定されます。
次に、'octopus' | 'tuna'
の'tuna'
部分について、こちらは'octopus' | 'squid'
の部分型ではないため、never
型と判定されます。
最後に、この2つの結果のユニオン型をとることで、Extract<'octopus' | 'squid', 'octopus' | 'tuna'>
が得られることになります。
得られる型は'octopus' | never
となるはずですが、never
型とのユニオンをとるとnever
型は消えてしまいますので(「空集合との和集合」と捉えるとわかりやすいです)、最終的に得られる型は'octopus'
型となるわけです。
以上のように少々難解な動きをするDistributive Conditional Types ですが、ユーティリティ型の実装にロジック部分が組み込まれることで、利用する敷居が下がっていてユーティリティ型となっている有用性を感じられますね。