TypeScript の可変長タプル型における共用体の分配2022年06月09日 01時29分

TypeScript の可変長タプル型 (variadic tuple types) とは、配列型やタプル型を展開して別のタプル型の一部として使える機能のことです (「TypeScript 4.0で導入されるVariadic Tuple Typesをさっそく使いこなす - Qiita」に詳しいです)。記法としては、展開する型の直前に三連続のドット ... を記述します。例えば、

type Sandwich<Fillings extends unknown[]> = ['bread', ...Fillings, 'bread'];

のように具材 (filling) をパン (bread) で挟む Sandwich 型があったとき、Sandwich<['ham']> 型は ['bread', 'ham', 'bread'] 型に展開されます。

type HamSandwich = Sandwich<['ham']>;
// → ['bread', 'ham', 'bread']

type BLTSandwich = Sandwich<['bacon', 'lettuce', 'tomato']>;
// → ['bread', 'bacon', 'lettuce', 'tomato', 'bread']

type RichHamSandwich = Sandwich<'ham'[]>;
// → ['bread', ...'ham'[], 'bread']

type TwoSlicesOfBread = Sandwich<[]>;
// → ['bread', 'bread']

ここで型引数 Fillings に共用体型 (縦線 | で区切った複数の型のうちのいずれかを表す型) を渡すとどうなるでしょうか。具材がハムか卵なら、できあがるのはハムサンドか卵サンドになります。つまり、Sandwich<['ham'] | ['egg']> 型は Sandwich<['ham']> | Sandwich<['egg']> 型と同等に扱われます。

type HamOrEggSandwich = Sandwich<['ham'] | ['egg']>;
// → ['bread', 'ham', 'bread'] | ['bread', 'egg', 'bread']

このように、T<A | B | C | (略)> 型が T<A> | T<B> | T<C> | (略) 型として扱われる挙動を「共用体の分配 (union distribution)」と呼びます。 共用体の分配は条件型 P extends Q ? R : S でも発生しますが (「TypeScriptの型初級 - Qiita」に詳しいです)、可変長タプル型でも発生するのです。

条件型における共用体の分配では、never 型を分配しようとすると展開結果も never 型になります。これは可変長タプル型においても同じです。

type ImpossibleSandwich = Sandwich<never>
// → never

なお、可変長タプル型において any 型は any[] 型に展開されます。unknown 型を展開することはできません。

type AnySandwich = Sandwich<any>;
// → ['bread', ...any[], 'bread']

type UnknownSandwich = Sandwich<unknown>;
// → Error: Type 'unknown' does not satisfy the constraint 'unknown[]'.

可変長タプル型で共用体の分配が発生するというのは、可変長タプル型を導入した pull request の説明に書かれています。私はこの挙動を、type-challenges の回答のひとつをきっかけに知りました (その回答は誤答なのですが)。

  • When the type argument for T is a union type, the union is spread over the tuple type. For example, [A, ...T, B] instantiated with X | Y | Z as the type argument for T yields a union of instantiations of [A, ...T, B] with X, Y and Z as the type argument for T respectively.

Variadic tuple types by ahejlsberg · Pull Request #39094 · microsoft/TypeScript